From 70289809e230b1239b6ed9a14414dc0d4a71a121 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 26 Nov 2025 15:22:16 +0800 Subject: [PATCH 1/2] feat: mcp toool --- README.md | 34 +++ package.json | 57 +++- src/extension.ts | 6 + src/languageModelTool.ts | 630 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 724 insertions(+), 3 deletions(-) create mode 100644 src/languageModelTool.ts diff --git a/README.md b/README.md index 54e122f1..040536ef 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ A lightweight Java Debugger based on [Java Debug Server](https://github.com/Micr - Debug console - Evaluation - Hot Code Replace +- **[NEW]** No-Config Debug (debug Java apps without launch.json) +- **[NEW]** AI-Assisted Debugging (GitHub Copilot integration) ## Requirements - JDK (version 1.8.0 or later) @@ -41,6 +43,38 @@ ext install vscode-java-debug Please also check the documentation of [Language Support for Java by Red Hat](https://marketplace.visualstudio.com/items?itemName=redhat.java) if you have trouble setting up your project. +## No-Config Debug + +You can now debug Java applications without creating a `launch.json` file! Simply open a terminal in VS Code and use the `debugjava` command: + +```bash +# Debug a main class +debugjava -cp bin com.example.Main + +# Debug a JAR file +debugjava -jar target/myapp.jar + +# Debug with arguments +debugjava -cp bin com.example.Main arg1 arg2 +``` + +The debugger will automatically attach. See [No-Config Debug Documentation](bundled/scripts/noConfigScripts/README.md) for more details. + +## AI-Assisted Debugging + +When using GitHub Copilot Chat, you can now ask AI to help you debug Java applications! The extension provides a Language Model Tool that enables natural language debugging: + +- "Debug my Spring Boot application" +- "Debug the Main class in this project" +- "Debug Calculator with arguments 10 and 5" + +The AI will automatically: +1. Detect your project type (Maven/Gradle/VS Code) +2. Build/compile your project +3. Start debugging with appropriate configuration + +See [Language Model Tool Documentation](docs/LANGUAGE_MODEL_TOOL.md) for more details. + ## Options ### Launch diff --git a/package.json b/package.json index 9cadc3c8..6d4487c2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "debugger" ], "engines": { - "vscode": "^1.75.0" + "vscode": "^1.95.0" }, "license": "SEE LICENSE IN LICENSE.txt", "repository": { @@ -42,7 +42,8 @@ "onDebugInitialConfigurations", "onDebugResolve:java", "onCommand:JavaDebug.SpecifyProgramArgs", - "onCommand:JavaDebug.PickJavaProcess" + "onCommand:JavaDebug.PickJavaProcess", + "onLanguageModelTool:debug_java_application" ], "main": "./dist/extension", "contributes": { @@ -985,7 +986,57 @@ "default": false } } - } + }, + "languageModelTools": [ + { + "name": "debug_java_application", + "displayName": "Debug Java Application", + "modelDescription": "Debug a Java application with proper classpath resolution. CRITICAL WORKFLOW: 1) FIRST call 'rebuild_java_project' tool to compile the project and get the correct classpath (outputPath), 2) THEN call this tool with the classpath from rebuild result. This ensures accurate classpath resolution. The tool starts debugging with 'debugjava' command and attaches the debugger automatically. IMPORTANT: If user requests debugging, you MUST call rebuild_java_project FIRST to get classpath, then pass it to this tool via the 'classpath' parameter. Set 'skipBuild: true' since rebuild was already done. The debug process will run indefinitely until the user stops it. Examples: 'Debug App' (after rebuild), 'Debug com.example.Main with args' (after rebuild), 'Debug target/app.jar'.", + "toolReferenceName": "debugJavaApplication", + "tags": [ + "java", + "debug", + "debugger", + "build", + "compile" + ], + "icon": "$(debug-alt)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "target": { + "type": "string", + "description": "What to debug: 1) Main class name - can be simple (e.g., 'App') or fully qualified (e.g., 'com.example.Main'). Tool auto-detects package from source files. 2) JAR file path (e.g., 'target/app.jar'). 3) Raw java arguments (e.g., '-cp bin com.example.Main'). IMPORTANT: For simple class names without packages, just use 'App' - the tool will find the .class file automatically." + }, + "workspacePath": { + "type": "string", + "description": "Absolute path to the Java project root directory containing pom.xml, build.gradle, or .java source files. This is the working directory for compilation and debugging." + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional command-line arguments to pass to the Java main method (e.g., ['arg1', 'arg2', '--flag=value']). These are program arguments, not JVM arguments." + }, + "skipBuild": { + "type": "boolean", + "description": "Whether to skip compilation before debugging. DEFAULT: false (will compile automatically). Set to true when you have already called 'rebuild_java_project' tool and obtained the classpath. Set to false if rebuilding within this tool is needed (fallback scenario).", + "default": false + }, + "classpath": { + "type": "string", + "description": "CRITICAL: The classpath obtained from 'rebuild_java_project' tool's outputPath field. This ensures accurate classpath resolution. REQUIRED when skipBuild is true. Format: absolute paths separated by system path delimiter (';' on Windows, ':' on Unix). Example: 'C:\\project\\target\\classes;C:\\project\\lib\\dep.jar' or '/project/target/classes:/project/lib/dep.jar'. If not provided, tool will attempt to infer classpath (less accurate)." + } + }, + "required": [ + "target", + "workspacePath" + ] + } + } + ] }, "scripts": { "vscode:prepublish": "npm run build", diff --git a/src/extension.ts b/src/extension.ts index 867b2c02..2ef16796 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { handleHotCodeReplaceCustomEvent, initializeHotCodeReplace, NO_BUTTON, Y import { JavaDebugAdapterDescriptorFactory } from "./javaDebugAdapterDescriptorFactory"; import { JavaInlineValuesProvider } from "./JavaInlineValueProvider"; import { logJavaException, logJavaInfo } from "./javaLogger"; +import { registerLanguageModelTool } from "./languageModelTool"; import { IMainClassOption, IMainMethod, resolveMainMethod } from "./languageServerPlugin"; import { mainClassPicker } from "./mainClassPicker"; import { pickJavaProcess } from "./processPicker"; @@ -40,6 +41,11 @@ export async function activate(context: vscode.ExtensionContext): Promise { ); context.subscriptions.push(noConfigDisposable); + // Register Language Model Tool for AI-assisted debugging + if (vscode.lm && vscode.lm.registerTool) { + registerLanguageModelTool(context); + } + return instrumentOperation("activation", initializeExtension)(context); } diff --git a/src/languageModelTool.ts b/src/languageModelTool.ts new file mode 100644 index 00000000..acbce8c7 --- /dev/null +++ b/src/languageModelTool.ts @@ -0,0 +1,630 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { sendError, sendInfo } from "vscode-extension-telemetry-wrapper"; + +interface DebugJavaApplicationInput { + target: string; + workspacePath: string; + args?: string[]; + skipBuild?: boolean; + classpath?: string; +} + +interface DebugJavaApplicationResult { + success: boolean; + message: string; + terminalName?: string; +} + +// Type definitions for Language Model API (these will be in future VS Code versions) +// For now, we use 'any' to allow compilation with older VS Code types +interface LanguageModelTool { + invoke(options: { input: T }, token: vscode.CancellationToken): Promise; +} + +/** + * Registers the Language Model Tool for debugging Java applications. + * This allows AI assistants to help users debug Java code by invoking the debugjava command. + */ +export function registerLanguageModelTool(context: vscode.ExtensionContext): vscode.Disposable | undefined { + // Check if the Language Model API is available + const lmApi = (vscode as any).lm; + if (!lmApi || typeof lmApi.registerTool !== 'function') { + // Language Model API not available in this VS Code version + return undefined; + } + + const tool: LanguageModelTool = { + async invoke(options: { input: DebugJavaApplicationInput }, token: vscode.CancellationToken): Promise { + sendInfo('', { + operationName: 'languageModelTool.debugJavaApplication.invoke', + target: options.input.target, + skipBuild: options.input.skipBuild?.toString() || 'false', + }); + + try { + const result = await debugJavaApplication(options.input, token); + + // Format the message for AI - use simple text, not JSON + const message = result.success + ? `✓ ${result.message}` + : `✗ ${result.message}`; + + // Return result in the expected format - simple text part + return new (vscode as any).LanguageModelToolResult([ + new (vscode as any).LanguageModelTextPart(message) + ]); + } catch (error) { + sendError(error as Error); + + const errorMessage = error instanceof Error ? error.message : String(error); + + return new (vscode as any).LanguageModelToolResult([ + new (vscode as any).LanguageModelTextPart(`✗ Debug failed: ${errorMessage}`) + ]); + } + } + }; + + const disposable = lmApi.registerTool('debug_java_application', tool); + context.subscriptions.push(disposable); + return disposable; +} + +/** + * Main function to debug a Java application. + * This function handles: + * 1. Project type detection + * 2. Building the project if needed + * 3. Executing the debugjava command + */ +async function debugJavaApplication( + input: DebugJavaApplicationInput, + token: vscode.CancellationToken +): Promise { + if (token.isCancellationRequested) { + return { + success: false, + message: 'Operation cancelled by user' + }; + } + + // Validate workspace path + const workspaceUri = vscode.Uri.file(input.workspacePath); + if (!fs.existsSync(input.workspacePath)) { + return { + success: false, + message: `Workspace path does not exist: ${input.workspacePath}` + }; + } + + // Step 1: Detect project type + const projectType = detectProjectType(input.workspacePath); + + // Step 2: Build the project if needed + if (!input.skipBuild) { + const buildResult = await buildProject(workspaceUri, projectType, token); + if (!buildResult.success) { + return buildResult; + } + } + + // Step 3: Construct and execute the debugjava command + const debugCommand = constructDebugCommand(input, projectType); + + // Validate that we can construct a valid command + if (!debugCommand || debugCommand === 'debugjava') { + return { + success: false, + message: 'Failed to construct debug command. Please check the target parameter.' + }; + } + + // Step 4: Execute in terminal (non-blocking) + const terminal = vscode.window.createTerminal({ + name: 'Java Debug', + cwd: input.workspacePath, + hideFromUser: false, + isTransient: false // Keep terminal alive even after process exits + }); + + terminal.show(); + + // Send the command and return immediately - don't wait for process to finish + // This is crucial because the Java process will run until user stops it + terminal.sendText(debugCommand); + + // Give a brief moment for the command to start + await new Promise(resolve => setTimeout(resolve, 500)); + + // Build info message for AI + let targetInfo = input.target; + let warningNote = ''; + + if (input.target.endsWith('.jar')) { + targetInfo = input.target; + } else if (input.target.includes('.')) { + targetInfo = input.target; + } else { + // Simple class name - check if we successfully detected the full name + const detectedClassName = findFullyQualifiedClassName(input.workspacePath, input.target, projectType); + if (detectedClassName) { + targetInfo = `${detectedClassName} (detected from ${input.target})`; + } else { + targetInfo = input.target; + warningNote = ' ⚠️ Note: Could not auto-detect package name. If you see "ClassNotFoundException", please provide the fully qualified class name (e.g., "com.example.App" instead of "App").'; + } + } + + return { + success: true, + message: `Debug session started for ${targetInfo}. The Java application is now running in debug mode in terminal '${terminal.name}'. The VS Code debugger should attach automatically. You can set breakpoints in your Java source files.${warningNote}`, + terminalName: terminal.name + }; +} + +/** + * Detects the type of Java project based on build files present. + */ +function detectProjectType(workspacePath: string): 'maven' | 'gradle' | 'vscode' | 'unknown' { + if (fs.existsSync(path.join(workspacePath, 'pom.xml'))) { + return 'maven'; + } + + if (fs.existsSync(path.join(workspacePath, 'build.gradle')) || + fs.existsSync(path.join(workspacePath, 'build.gradle.kts'))) { + return 'gradle'; + } + + // Check if VS Code Java extension is likely managing compilation + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(workspacePath)); + if (workspaceFolder) { + const javaExt = vscode.extensions.getExtension('redhat.java'); + if (javaExt?.isActive) { + return 'vscode'; + } + } + + return 'unknown'; +} + +/** + * Builds the Java project based on its type. + */ +async function buildProject( + workspaceUri: vscode.Uri, + projectType: 'maven' | 'gradle' | 'vscode' | 'unknown', + _token: vscode.CancellationToken +): Promise { + switch (projectType) { + case 'maven': + return await buildMavenProject(workspaceUri); + + case 'gradle': + return await buildGradleProject(workspaceUri); + + case 'vscode': + return await ensureVSCodeCompilation(workspaceUri); + + case 'unknown': + // Try to proceed anyway - user might have manually compiled + return { + success: true, + message: 'Unknown project type. Skipping build step. Ensure your Java files are compiled.' + }; + } +} + +/** + * Builds a Maven project using mvn compile. + */ +async function buildMavenProject( + workspaceUri: vscode.Uri +): Promise { + return new Promise((resolve) => { + // Use task API for better control + const task = new vscode.Task( + { type: 'shell', task: 'maven-compile' }, + vscode.workspace.getWorkspaceFolder(workspaceUri)!, + 'Maven Compile', + 'Java Debug', + new vscode.ShellExecution('mvn compile', { cwd: workspaceUri.fsPath }) + ); + + // Set a timeout to avoid hanging indefinitely + const timeout = setTimeout(() => { + resolve({ + success: true, + message: 'Maven compile command sent. Build may still be in progress.' + }); + }, 60000); // 60 second timeout + + vscode.tasks.executeTask(task).then((execution) => { + let resolved = false; + + const disposable = vscode.tasks.onDidEndTask((e) => { + if (e.execution === execution && !resolved) { + resolved = true; + clearTimeout(timeout); + disposable.dispose(); + resolve({ + success: true, + message: 'Maven project compiled successfully' + }); + } + }); + + const errorDisposable = vscode.tasks.onDidEndTaskProcess((e) => { + if (e.execution === execution && e.exitCode !== 0 && !resolved) { + resolved = true; + clearTimeout(timeout); + errorDisposable.dispose(); + resolve({ + success: false, + message: `Maven build failed with exit code ${e.exitCode}. Please check the terminal output.` + }); + } + }); + }); + }); +} + +/** + * Builds a Gradle project using gradle classes. + */ +async function buildGradleProject( + workspaceUri: vscode.Uri +): Promise { + return new Promise((resolve) => { + const gradleWrapper = process.platform === 'win32' ? 'gradlew.bat' : './gradlew'; + const gradleCommand = fs.existsSync(path.join(workspaceUri.fsPath, gradleWrapper)) + ? gradleWrapper + : 'gradle'; + + const task = new vscode.Task( + { type: 'shell', task: 'gradle-classes' }, + vscode.workspace.getWorkspaceFolder(workspaceUri)!, + 'Gradle Classes', + 'Java Debug', + new vscode.ShellExecution(`${gradleCommand} classes`, { cwd: workspaceUri.fsPath }) + ); + + // Set a timeout to avoid hanging indefinitely + const timeout = setTimeout(() => { + resolve({ + success: true, + message: 'Gradle compile command sent. Build may still be in progress.' + }); + }, 60000); // 60 second timeout + + vscode.tasks.executeTask(task).then((execution) => { + let resolved = false; + + const disposable = vscode.tasks.onDidEndTask((e) => { + if (e.execution === execution && !resolved) { + resolved = true; + clearTimeout(timeout); + disposable.dispose(); + resolve({ + success: true, + message: 'Gradle project compiled successfully' + }); + } + }); + + const errorDisposable = vscode.tasks.onDidEndTaskProcess((e) => { + if (e.execution === execution && e.exitCode !== 0 && !resolved) { + resolved = true; + clearTimeout(timeout); + errorDisposable.dispose(); + resolve({ + success: false, + message: `Gradle build failed with exit code ${e.exitCode}. Please check the terminal output.` + }); + } + }); + }); + }); +} + +/** + * Ensures VS Code Java Language Server has compiled the files. + */ +async function ensureVSCodeCompilation(workspaceUri: vscode.Uri): Promise { + try { + // Check for compilation errors using VS Code diagnostics + const javaFiles = await vscode.workspace.findFiles( + new vscode.RelativePattern(workspaceUri, '**/*.java'), + '**/node_modules/**', + 100 // Limit to 100 files for performance + ); + + let hasErrors = false; + for (const file of javaFiles) { + const diagnostics = vscode.languages.getDiagnostics(file); + const errors = diagnostics.filter(d => d.severity === vscode.DiagnosticSeverity.Error); + if (errors.length > 0) { + hasErrors = true; + break; + } + } + + if (hasErrors) { + return { + success: false, + message: 'Compilation errors detected in the project. Please fix the errors before debugging.' + }; + } + + // Check if Java extension is active and in standard mode + const javaExt = vscode.extensions.getExtension('redhat.java'); + if (!javaExt?.isActive) { + return { + success: true, + message: 'Java Language Server is not active. Proceeding with debug, but ensure your code is compiled.' + }; + } + + return { + success: true, + message: 'VS Code Java compilation verified' + }; + } catch (error) { + // If we can't verify, proceed anyway + return { + success: true, + message: 'Unable to verify compilation status. Proceeding with debug.' + }; + } +} + +/** + * Constructs the debugjava command based on input parameters. + */ +function constructDebugCommand( + input: DebugJavaApplicationInput, + projectType: 'maven' | 'gradle' | 'vscode' | 'unknown' +): string { + let command = 'debugjava'; + + // Handle JAR files + if (input.target.endsWith('.jar')) { + command += ` -jar ${input.target}`; + } + // Handle raw java command arguments (starts with - like -cp, -jar, etc) + else if (input.target.startsWith('-')) { + command += ` ${input.target}`; + } + // Handle class name (with or without package) + else { + let className = input.target; + + // If target doesn't contain a dot and we can find the Java file, + // try to detect the fully qualified class name + if (!input.target.includes('.')) { + const detectedClassName = findFullyQualifiedClassName(input.workspacePath, input.target, projectType); + if (detectedClassName) { + sendInfo('', { + operationName: 'languageModelTool.classNameDetection', + simpleClassName: input.target, + detectedClassName: detectedClassName, + projectType: projectType + }); + className = detectedClassName; + } else { + // No package detected - class is in default package + sendInfo('', { + operationName: 'languageModelTool.classNameDetection.noPackage', + simpleClassName: input.target, + projectType: projectType + }); + } + } + + // Use provided classpath if available, otherwise infer it + const classpath = input.classpath || inferClasspath(input.workspacePath, projectType); + + command += ` -cp "${classpath}" ${className}`; + } + + // Add arguments if provided + if (input.args && input.args.length > 0) { + command += ' ' + input.args.join(' '); + } + + return command; +} + +/** + * Tries to find the fully qualified class name by searching for the Java file. + * This helps when user provides just "App" instead of "com.example.App". + */ +function findFullyQualifiedClassName( + workspacePath: string, + simpleClassName: string, + projectType: 'maven' | 'gradle' | 'vscode' | 'unknown' +): string | null { + // Determine source directories based on project type + const sourceDirs: string[] = []; + + switch (projectType) { + case 'maven': + sourceDirs.push(path.join(workspacePath, 'src', 'main', 'java')); + break; + case 'gradle': + sourceDirs.push(path.join(workspacePath, 'src', 'main', 'java')); + break; + case 'vscode': + sourceDirs.push(path.join(workspacePath, 'src')); + break; + case 'unknown': + // Try all common locations + sourceDirs.push( + path.join(workspacePath, 'src', 'main', 'java'), + path.join(workspacePath, 'src'), + workspacePath + ); + break; + } + + // Search for the Java file + for (const srcDir of sourceDirs) { + if (!fs.existsSync(srcDir)) { + continue; + } + + try { + const javaFile = findJavaFile(srcDir, simpleClassName); + if (javaFile) { + // Extract package name from the file + const packageName = extractPackageName(javaFile); + if (packageName) { + return `${packageName}.${simpleClassName}`; + } else { + // No package, use simple name + return simpleClassName; + } + } + } catch (error) { + // Continue searching in other directories + } + } + + return null; +} + +/** + * Recursively searches for a Java file with the given class name. + */ +function findJavaFile(dir: string, className: string): string | null { + try { + const files = fs.readdirSync(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + // Skip common non-source directories + if (file === 'node_modules' || file === '.git' || file === 'target' || file === 'build') { + continue; + } + const found = findJavaFile(filePath, className); + if (found) { + return found; + } + } else if (file === `${className}.java`) { + return filePath; + } + } + } catch (error) { + // Ignore permission errors or other file system issues + } + + return null; +} + +/** + * Extracts the package name from a Java source file. + */ +function extractPackageName(javaFilePath: string): string | null { + try { + const content = fs.readFileSync(javaFilePath, 'utf-8'); + const packageMatch = content.match(/^\s*package\s+([\w.]+)\s*;/m); + return packageMatch ? packageMatch[1] : null; + } catch (error) { + return null; + } +} + +/** + * Checks if a directory contains any .class files. + */ +function hasClassFiles(dir: string): boolean { + try { + const files = fs.readdirSync(dir); + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isFile() && file.endsWith('.class')) { + return true; + } else if (stat.isDirectory()) { + // Recursively check subdirectories (but limit depth) + if (hasClassFiles(filePath)) { + return true; + } + } + } + } catch (error) { + // Ignore errors + } + return false; +} + +/** + * Infers the classpath based on project type and common conventions. + */ +function inferClasspath(workspacePath: string, projectType: 'maven' | 'gradle' | 'vscode' | 'unknown'): string { + const classpaths: string[] = []; + + switch (projectType) { + case 'maven': + // Maven standard output directory + const mavenTarget = path.join(workspacePath, 'target', 'classes'); + if (fs.existsSync(mavenTarget)) { + classpaths.push(mavenTarget); + } + break; + + case 'gradle': + // Gradle standard output directories + const gradleMain = path.join(workspacePath, 'build', 'classes', 'java', 'main'); + if (fs.existsSync(gradleMain)) { + classpaths.push(gradleMain); + } + break; + + case 'vscode': + // VS Code Java extension default output + const vscodeOut = path.join(workspacePath, 'bin'); + if (fs.existsSync(vscodeOut)) { + classpaths.push(vscodeOut); + } + break; + } + + // Fallback to common locations + if (classpaths.length === 0) { + const commonPaths = [ + path.join(workspacePath, 'bin'), // VS Code default + path.join(workspacePath, 'out'), // IntelliJ default + path.join(workspacePath, 'target', 'classes'), // Maven + path.join(workspacePath, 'build', 'classes', 'java', 'main'), // Gradle + path.join(workspacePath, 'build', 'classes'), + ]; + + // Check each common path + for (const p of commonPaths) { + if (fs.existsSync(p)) { + // Check if there are actually .class files in this directory + if (hasClassFiles(p)) { + classpaths.push(p); + break; + } + } + } + } + + // If still no classpath found, use current directory + // This is common for simple projects where .class files are alongside .java files + if (classpaths.length === 0) { + classpaths.push('.'); + } + + return classpaths.join(path.delimiter); +} From 7812075ea7ad4a5f209b507969fa6150521d5905 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Thu, 27 Nov 2025 09:32:57 +0800 Subject: [PATCH 2/2] feat: add agent.md --- bundled/agents/debug.agent.md | 252 ++++++++++++++++++++++++++++++++++ package.json | 11 +- 2 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 bundled/agents/debug.agent.md diff --git a/bundled/agents/debug.agent.md b/bundled/agents/debug.agent.md new file mode 100644 index 00000000..bcc1e53a --- /dev/null +++ b/bundled/agents/debug.agent.md @@ -0,0 +1,252 @@ + +# Debug Orchestrator for Java (VS Code + No‑Config Debug) + +> This agent integrates with the VS Code Debug extension’s **languageModelTools** to implement the critical workflow: +> 1) **First call** `java_project_rebuild` (toolReferenceName: `java-project-rebuild`) to perform a clean rebuild and return the **classpath** (`outputPath`). +> 2) **Then call** `debug_java_application` (toolReferenceName: `debugJavaApplication`) with `skipBuild: true` and pass the classpath from step 1. +> +> Internally, the tool uses `debugjava` to start the JVM and automatically attach the VS Code Java debugger. The agent monitors breakpoints, stack traces, variables, and execution flow using VS Code Debug APIs and DAP. + +--- + +## 🎯 Goals +- Start Java debugging in VS Code **without `launch.json`** (No‑Config Debug) and auto-attach. +- Enforce a unified workflow: **rebuild → debug → monitor/control → (optional) adjust logging level or hand off to a coding agent for fixes**. +- Use **context-isolated subagents** for safe separation of responsibilities: breakpoint management, diagnostics, and logging level changes. + +--- + +## 🧩 Integrated languageModelTools + +### `java_project_rebuild` (toolReferenceName: `java-project-rebuild`) +**Purpose**: Trigger a clean rebuild to recompile source files and resolve dependencies. Can rebuild all projects in the workspace or a specific project. + +**Input** (optional): +- `resourcePath: string` — File path or URI to determine which project to rebuild. If omitted, rebuilds all projects in the workspace. + +**Output (expected)**: +- `outputPath: string` — Accurate classpath (absolute paths separated by `:` on Unix or `;` on Windows). + +> Note: `debug_java_application` requires the `outputPath` from this tool as its `classpath` argument when `skipBuild` is true. + +--- + +### `debug_java_application` (toolReferenceName: `debugJavaApplication`) +**Purpose**: Start a Java application using `debugjava` and attach the debugger automatically. The session runs until the user stops it. + +**Workflow requirement**: Must call `java_project_rebuild` first to get the classpath. + +**Input**: +- `target: string` (required) — One of: + 1) Main class name (simple or fully qualified, e.g., `App` or `com.example.Main`). + 2) JAR file path (e.g., `build/libs/app.jar`). + 3) Raw Java arguments (e.g., `-cp bin com.example.Main`). +- `workspacePath: string` (required) — Absolute path to the project root. +- `args: string[]` (optional) — Program arguments for `main(String[])`. +- `skipBuild: boolean` (default: false) — Set to **true** after rebuild. +- `classpath: string` — Required when `skipBuild` is true; obtained from `outputPath`. + +**Behavior**: +- Internally runs `debugjava` → JVM starts with JDWP → VS Code auto-attaches the debugger. + +--- + +## 🔄 Debugging Workflow (Agent Orchestration) + +### 1) Detect intent → choose launch mode +Examples: +- “Debug `com.example.app.Application` with logging level DEBUG” +- “Debug JAR and set port to 8081” + +### 2) **Rebuild first** (compute classpath) +```pseudo +call tool: java-project-rebuild + with: { resourcePath?: "/absolute/path/to/project" } +receive: { outputPath: CP } # Accurate classpath +cache: CP +``` + +### 3) **Start No‑Config Debug** (auto-attach) +```pseudo +call tool: debugJavaApplication + with: { + target: "com.example.app.Application" | "build/libs/app.jar" | "-cp ... Main", + workspacePath: "/absolute/path/to/project", + args: ["--server.port=8081", "--logging.level.root=DEBUG"], + skipBuild: true, + classpath: CP + } +# debugjava starts → VS Code auto-attaches → session active +``` + +### 4) **Monitor and control the session** (VS Code Debug API / DAP) +- Subscribe to: `onDidStartDebugSession`, `onDidTerminateDebugSession`, `stopped` events. +- On breakpoint hit (`stopped`): + - Request `stackTrace → scopes → variables` + - Render summary in Chat (thread, top frames, key variables). +- Execution control: `continue`, `stepOver`, `stepInto`, `stepOut`, `pause`. +- Breakpoint management: list/add/remove line, conditional, and logpoints. +- Evaluate expressions: `evaluate { expression: "a + b" }`. + +### 5) **Adjust logging level** (two options) +- **Restart with args** (simple and reliable): + - Stop current session → call `debugJavaApplication` again with updated `args`: + `--logging.level.root=TRACE --logging.level.com.example.app=DEBUG` +- **Hot change via Spring Boot Actuator** (if enabled): + - `POST /actuator/loggers/{logger}` with `{ "configuredLevel": "DEBUG" }`. + +### 6) **(Optional) Handoff to Coding Agent** +- Provide context (stack, variables, file/line, fix intent) to Coding Agent. +- Coding Agent applies changes, rebuilds, runs tests, creates PR. +- Return to this agent to verify fix. + +--- + +## 🔒 Context-Isolated Subagents (Recommended) + +### Subagent: **Breakpoints** +- Capabilities: read/write breakpoints, conditional/logpoints. +- No network or file write permissions. + +### Subagent: **Diagnostics** +- Capabilities: read-only DAP data (threads, stack, variables), summarize for Chat. +- Cannot modify code or breakpoints. + +### Subagent: **Logging** +- Capabilities: restart with new args or call Actuator endpoint to change log level. +- No code editing. + +--- + +## 🛠 Agent Tool Declarations (Mapping to languageModelTools and VS Code APIs) + +### `invokeRebuild` +```tool +name: invokeRebuild +description: Call languageModelTool `java-project-rebuild` to perform a clean rebuild and return classpath. +input_schema: + type: object + properties: + resourcePath: { type: string } +impl: + kind: languageModelTool.invoke + toolReferenceName: java-project-rebuild +output_schema: + type: object + properties: + outputPath: { type: string, description: "Accurate classpath" } +``` + +### `startNoConfigDebug` +```tool +name: startNoConfigDebug +description: Call `debugJavaApplication` (skipBuild=true) with classpath from rebuild to start debugjava and auto-attach. +input_schema: + type: object + properties: + target: { type: string } + workspacePath: { type: string } + args: { type: array, items: { type: string } } + classpath: { type: string } + required: [target, workspacePath, classpath] +impl: + kind: languageModelTool.invoke + toolReferenceName: debugJavaApplication + fixed_args: { skipBuild: true } +``` + +### `watchSession` +```tool +name: watchSession +description: Subscribe to VS Code debug session events; on 'stopped', fetch stack/scopes/variables and summarize. +impl: + kind: vscode.debug.subscribe +``` + +### `dapControl` +```tool +name: dapControl +description: Send a DAP command to the active session (continue, stepOver, stepInto, pause, evaluate). +input_schema: + type: object + properties: + command: { type: string, enum: ["continue","pause","stepOver","stepInto","stepOut","evaluate"] } + arguments: { type: object } + required: [command] +impl: + kind: vscode.debug.customRequest +``` + +### `breakpoints` +```tool +name: breakpoints +description: List/add/remove/update breakpoints and logpoints. +impl: + kind: vscode.debug.manageBreakpoints +``` + +### `switchLogLevel` +```tool +name: switchLogLevel +description: Adjust logging level via restart (args) or Spring Boot Actuator. +input_schema: + type: object + properties: + mode: { type: string, enum: ["restart","actuator"] } + root: { type: string, enum: ["TRACE","DEBUG","INFO","WARN","ERROR"] } + packages: + type: array + items: { type: object, properties: { logger: { type: string }, level: { type: string } } } + actuatorUrl: { type: string } +impl: + kind: composite + steps: + - when: "mode == 'restart'" + run: languageModelTool.invoke + toolReferenceName: debugJavaApplication + argsFromContext: + target: "" + workspacePath: "" + classpath: "" + skipBuild: true + args: "--logging.level.root=${root} ${packages.map(p => `--logging.level.${p.logger}=${p.level}`).join(' ')}" + - when: "mode == 'actuator'" + forEach: "packages" + run: http.request + request: + method: "POST" + url: "${actuatorUrl}/${item.logger}" + json: { configuredLevel: "${item.level}" } +``` + +--- + +## 🧪 Prompt → Flow Examples +- **“Rebuild and debug `com.example.app.Application` with logging level DEBUG”**: + 1) `invokeRebuild({ resourcePath: "/abs/project" })` → `CP` + 2) `startNoConfigDebug({ target: "com.example.app.Application", workspacePath: "/abs/project", classpath: CP, args: ["--logging.level.root=DEBUG"] })` + 3) `watchSession()` → On breakpoint hit, show stack and variables. + +- **“Debug JAR with port 8081 and log variables on breakpoint”**: + 1) `invokeRebuild({})` → `CP` + 2) `startNoConfigDebug({ target: "build/libs/app.jar", workspacePath: "/abs/project", classpath: CP, args: ["--server.port=8081"] })` + 3) `breakpoints.add({ file: "CalcService.java", line: 18, logMessage: "a=${a}, b=${b}" })` + 4) `dapControl({ command: "stepOver" })` + +- **“Change log level for `com.example.app` to TRACE (no breakpoint)”**: + - `switchLogLevel({ mode: "restart", root: "INFO", packages: [{ logger: "com.example.app", level: "TRACE" }] })` + +--- + +## ⚠️ Notes +- Always follow the sequence: **rebuild → debug**. +- Accurate classpath is critical for debugging; ensure `outputPath` is absolute and properly formatted. +- No‑Config Debug and `launch.json` are not mutually exclusive; keep `launch.json` for remote attach or repeatable tasks if needed. + +--- + +## 📋 How does this agent implement debugging? +1. **Build**: Calls `java-project-rebuild` to get accurate classpath. +2. **Start**: Calls `debugJavaApplication` with `skipBuild=true` and `classpath` → extension runs `debugjava` → VS Code auto-attaches. +3. **Monitor/control**: Subscribes to debug events and uses DAP for stack/variables, execution control, and breakpoints. +4. **Adjust logging**: Restart with args or use Actuator; optionally hand off to Coding Agent for code fixes and PR creation. diff --git a/package.json b/package.json index 6d4487c2..7122c58c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "debugger" ], "engines": { - "vscode": "^1.95.0" + "vscode": "^1.106.3" }, "license": "SEE LICENSE IN LICENSE.txt", "repository": { @@ -1036,6 +1036,13 @@ ] } } + ], + "chatAgents": [ + { + "name": "JavaDebugAgent", + "path": "./bundled/agents/debug.agent.md", + "description": "Custome Agent for debugging Java applications." + } ] }, "scripts": { @@ -1072,4 +1079,4 @@ "vscode-languageserver-types": "3.16.0", "vscode-tas-client": "^0.1.84" } -} +} \ No newline at end of file