|
| 1 | +import { Task } from "../task/Task" |
| 2 | +import { |
| 3 | + DebugToolUse, |
| 4 | + AskApproval, |
| 5 | + HandleError, |
| 6 | + PushToolResult, |
| 7 | + // ToolResponse, // Not explicitly used for a variable's type |
| 8 | + // Assuming RemoveClosingTag might not be needed for debug tool, but can add if necessary |
| 9 | +} from "../../shared/tools" |
| 10 | +import { formatResponse } from "../prompts/responses" // For consistent error/result formatting |
| 11 | +import { ClineSayTool } from "../../shared/ExtensionMessage" // For type checking with satisfies |
| 12 | + |
| 13 | +import { outputChannel } from "../../roo_debug/src/vscodeUtils" |
| 14 | +import { |
| 15 | + vsCodeDebugController, |
| 16 | + IDebugController, |
| 17 | + ToggleBreakpointParams, // Used in createOperationMap |
| 18 | + // Types are now primarily used in debugToolValidation.ts |
| 19 | +} from "../../roo_debug" // Import from the new index file |
| 20 | +import { getLastSessionDapOutput, getLastSessionRawTerminalOutput } from "../../roo_debug/src/controller/session" // Added for IV.C |
| 21 | +import { XMLParser } from "fast-xml-parser" // Added for XML parsing |
| 22 | +import { validateOperationArgs } from "./debugToolValidation" |
| 23 | + |
| 24 | +// Type for the operation map values |
| 25 | +type DebugOperationFn = (args?: any) => Promise<any> |
| 26 | + |
| 27 | +// Initialize all objects at module level |
| 28 | +// This prevents them from being recreated for each function call |
| 29 | +// and from being included in the state sent to the webview |
| 30 | +const moduleController: IDebugController = vsCodeDebugController |
| 31 | +// Create the operation map once at module level |
| 32 | +const moduleOperationMap = createOperationMap(moduleController) |
| 33 | +// Create XML parser once at module level |
| 34 | +const moduleXmlParser = new XMLParser({ |
| 35 | + ignoreAttributes: false, |
| 36 | + attributeNamePrefix: "@_", |
| 37 | + textNodeName: "_text", |
| 38 | + parseTagValue: true, |
| 39 | + parseAttributeValue: true, |
| 40 | +}) |
| 41 | + |
| 42 | +// Helper to create the operation map with correct 'this' binding |
| 43 | +function createOperationMap(controller: IDebugController): Record<string, DebugOperationFn> { |
| 44 | + return { |
| 45 | + launch: controller.launch.bind(controller), |
| 46 | + restart: controller.restart.bind(controller), |
| 47 | + quit: controller.quit.bind(controller), |
| 48 | + continue: controller.continue.bind(controller), |
| 49 | + next: controller.next.bind(controller), |
| 50 | + step_in: controller.stepIn.bind(controller), |
| 51 | + step_out: controller.stepOut.bind(controller), |
| 52 | + jump: controller.jump.bind(controller), |
| 53 | + until: controller.until.bind(controller), |
| 54 | + set_breakpoint: controller.setBreakpoint.bind(controller), |
| 55 | + set_temp_breakpoint: controller.setTempBreakpoint.bind(controller), |
| 56 | + remove_breakpoint: controller.removeBreakpointByLocation.bind(controller), |
| 57 | + remove_all_breakpoints_in_file: controller.removeAllBreakpointsInFile.bind(controller), |
| 58 | + disable_breakpoint: (params: ToggleBreakpointParams) => controller.disableBreakpoint(params), // Explicitly typing for clarity |
| 59 | + enable_breakpoint: (params: ToggleBreakpointParams) => controller.enableBreakpoint(params), // Explicitly typing for clarity |
| 60 | + ignore_breakpoint: controller.ignoreBreakpoint.bind(controller), |
| 61 | + set_breakpoint_condition: controller.setBreakpointCondition.bind(controller), |
| 62 | + get_active_breakpoints: controller.getActiveBreakpoints.bind(controller), |
| 63 | + stack_trace: controller.stackTrace.bind(controller), |
| 64 | + list_source: controller.listSource.bind(controller), |
| 65 | + up: controller.up.bind(controller), |
| 66 | + down: controller.down.bind(controller), |
| 67 | + goto_frame: controller.gotoFrame.bind(controller), |
| 68 | + get_source: controller.getSource.bind(controller), |
| 69 | + get_stack_frame_variables: controller.getStackFrameVariables.bind(controller), |
| 70 | + get_args: controller.getArgs.bind(controller), |
| 71 | + evaluate: controller.evaluate.bind(controller), |
| 72 | + pretty_print: controller.prettyPrint.bind(controller), |
| 73 | + whatis: controller.whatis.bind(controller), |
| 74 | + execute_statement: controller.executeStatement.bind(controller), |
| 75 | + get_last_stop_info: controller.getLastStopInfo.bind(controller), |
| 76 | + // IV.C: Add tools for DAP and Raw Terminal output |
| 77 | + debug_get_session_dap_output: async (args: { sessionId: string }) => { |
| 78 | + // These functions are not on IDebugController, but directly exported from session.ts |
| 79 | + const output = getLastSessionDapOutput(args.sessionId) |
| 80 | + return { success: true, output: output ?? null } // Ensure null if undefined |
| 81 | + }, |
| 82 | + debug_get_session_raw_terminal_output: async (args: { sessionId: string }) => { |
| 83 | + const output = getLastSessionRawTerminalOutput(args.sessionId) |
| 84 | + return { success: true, output: output ?? null } // Ensure null if undefined |
| 85 | + }, |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +export async function debugTool( |
| 90 | + cline: Task, |
| 91 | + block: DebugToolUse, |
| 92 | + askApproval: AskApproval, |
| 93 | + handleError: HandleError, |
| 94 | + pushToolResult: PushToolResult, |
| 95 | +): Promise<void> { |
| 96 | + const { debug_operation, _text, ...otherParams } = block.params // Extract _text |
| 97 | + // Use the module-level controller, operationMap, and xmlParser |
| 98 | + |
| 99 | + try { |
| 100 | + //outputChannel.appendLine(`[Debug Tool] Raw tool use block: ${JSON.stringify(block, null, 2)}`) |
| 101 | + outputChannel.appendLine( |
| 102 | + `[Debug Tool] Processing operation '${debug_operation}'. Raw params: ${JSON.stringify(block.params)}`, |
| 103 | + ) |
| 104 | + |
| 105 | + if (!debug_operation) { |
| 106 | + cline.consecutiveMistakeCount++ |
| 107 | + // Debug tool errors are tracked separately since "debug" is not a valid ToolName |
| 108 | + // We'll use execute_command as a proxy for error tracking |
| 109 | + cline.recordToolError("execute_command") |
| 110 | + pushToolResult(await cline.sayAndCreateMissingParamError("execute_command", "debug_operation")) |
| 111 | + return |
| 112 | + } |
| 113 | + |
| 114 | + // Determine content for approval prompt |
| 115 | + let approvalDisplayContent: string |
| 116 | + if (typeof _text === "string" && _text.trim().length > 0) { |
| 117 | + // Check if _text is a non-empty string |
| 118 | + try { |
| 119 | + // Try to parse and pretty-print JSON for approval |
| 120 | + const parsedJsonPayload = JSON.parse(_text) |
| 121 | + approvalDisplayContent = JSON.stringify(parsedJsonPayload, null, 2) |
| 122 | + outputChannel.appendLine( |
| 123 | + `[Debug Tool] Using _text (JSON) for approval prompt: ${approvalDisplayContent}`, |
| 124 | + ) |
| 125 | + } catch (e) { |
| 126 | + // If _text is present but fails to parse as JSON, show raw _text for approval, |
| 127 | + // but actual parsing later will fail if it's not valid JSON. |
| 128 | + approvalDisplayContent = _text |
| 129 | + outputChannel.appendLine( |
| 130 | + `[Debug Tool] _text failed JSON.parse for approval, showing raw: ${approvalDisplayContent}. Error will be caught during actual parsing if invalid.`, |
| 131 | + ) |
| 132 | + } |
| 133 | + } else { |
| 134 | + // If _text is not a string, or is an empty string, then no arguments are provided. |
| 135 | + approvalDisplayContent = "(No arguments)" |
| 136 | + outputChannel.appendLine( |
| 137 | + `[Debug Tool] No _text content for arguments. Approval prompt shows: ${approvalDisplayContent}`, |
| 138 | + ) |
| 139 | + } |
| 140 | + |
| 141 | + // Since "debug" is not a valid tool in ClineSayTool, we'll use a different approach |
| 142 | + // We'll just pass the content directly without trying to match ClineSayTool interface |
| 143 | + const completeMessage = JSON.stringify({ |
| 144 | + tool: "execute_command", // Use execute_command as a proxy since debug is not valid |
| 145 | + content: approvalDisplayContent, |
| 146 | + // Store debug_operation in content or as metadata |
| 147 | + }) |
| 148 | + |
| 149 | + outputChannel.appendLine(`[Debug Tool] Approval prompt prepared: ${completeMessage}`) |
| 150 | + outputChannel.appendLine(`[Debug Tool] About to call askApproval.`) |
| 151 | + |
| 152 | + let didApprove = false |
| 153 | + try { |
| 154 | + // outputChannel.appendLine( |
| 155 | + // `[Debug Tool] Calling askApproval with type "tool" and message: ${completeMessage}`, |
| 156 | + // ) |
| 157 | + didApprove = await askApproval("tool", completeMessage) |
| 158 | + //outputChannel.appendLine(`[Debug Tool] askApproval returned: ${didApprove}`) |
| 159 | + } catch (approvalError: any) { |
| 160 | + outputChannel.appendLine(`[ERROR][debugTool] Error during askApproval: ${approvalError.message}`) |
| 161 | + await handleError(`asking approval for debug operation '${debug_operation}'`, approvalError) |
| 162 | + pushToolResult(formatResponse.toolError(`Error asking for approval: ${approvalError.message}`)) |
| 163 | + return |
| 164 | + } |
| 165 | + |
| 166 | + // Add a message to the chat to show what was approved/denied |
| 167 | + if (didApprove) { |
| 168 | + await cline.say("text", `Debug operation approved: ${debug_operation}`) |
| 169 | + } else { |
| 170 | + await cline.say("text", `Debug operation denied: ${debug_operation}`) |
| 171 | + } |
| 172 | + |
| 173 | + if (!didApprove) { |
| 174 | + // User denied the operation |
| 175 | + pushToolResult(formatResponse.toolError(`User denied debugger operation: ${debug_operation}`)) |
| 176 | + return |
| 177 | + } |
| 178 | + |
| 179 | + // Only proceed with parsing and validation if approval is granted |
| 180 | + let operationArgs: any = {} |
| 181 | + |
| 182 | + // Parameters must now come from the JSON payload in _text. |
| 183 | + if (typeof _text === "string" && _text.trim().length > 0) { |
| 184 | + outputChannel.appendLine(`[Debug Tool] Attempting to parse _text as JSON: ${_text}`) |
| 185 | + try { |
| 186 | + operationArgs = JSON.parse(_text) |
| 187 | + // Ensure operationArgs is an object if it parsed to null, |
| 188 | + // or handle cases where it might parse to a primitive if that's valid for some ops. |
| 189 | + // For most debug operations, an object (even empty) or an array is expected. |
| 190 | + if (operationArgs === null) { |
| 191 | + operationArgs = {} // Treat JSON "null" as empty args object for consistency |
| 192 | + outputChannel.appendLine(`[Debug Tool] _text parsed to null, defaulting operationArgs to {}.`) |
| 193 | + } else if (typeof operationArgs !== "object" && !Array.isArray(operationArgs)) { |
| 194 | + // If JSON parsed to a primitive (string, number, boolean) |
| 195 | + // This is generally unexpected for debug operations that take multiple params. |
| 196 | + // The validation step should catch if this type is inappropriate for the specific operation. |
| 197 | + // If an operation legitimately takes a single primitive, validation should allow it. |
| 198 | + // Otherwise, validation should fail. |
| 199 | + outputChannel.appendLine( |
| 200 | + `[Debug Tool] _text parsed to a non-object/non-array primitive: ${typeof operationArgs}. Validation will determine if this is acceptable for '${debug_operation}'.`, |
| 201 | + ) |
| 202 | + } |
| 203 | + outputChannel.appendLine( |
| 204 | + `[Debug Tool] Successfully parsed _text as JSON. operationArgs: ${JSON.stringify(operationArgs)}`, |
| 205 | + ) |
| 206 | + } catch (e) { |
| 207 | + await handleError(`parsing JSON content for debug operation ${debug_operation}`, e as Error) |
| 208 | + pushToolResult( |
| 209 | + formatResponse.toolError( |
| 210 | + `Invalid JSON content provided for operation '${debug_operation}': ${(e as Error).message}. Parameters must be a valid JSON object or array within the operation tag.`, |
| 211 | + ), |
| 212 | + ) |
| 213 | + return |
| 214 | + } |
| 215 | + } else { |
| 216 | + // No _text provided, or it's an empty string. |
| 217 | + // This means no arguments are passed for the operation. |
| 218 | + // Some operations are valid without arguments (e.g., quit, continue). |
| 219 | + // Validation will check if arguments are required for the specific 'debug_operation'. |
| 220 | + outputChannel.appendLine( |
| 221 | + `[Debug Tool] No JSON _text content found or _text is empty. Assuming no arguments for '${debug_operation}'. Validation will check if args are required.`, |
| 222 | + ) |
| 223 | + operationArgs = {} // Default to empty object if no _text |
| 224 | + } |
| 225 | + |
| 226 | + // Validate arguments after approval |
| 227 | + //outputChannel.appendLine(`[Debug Tool] About to validate arguments for '${debug_operation}'.`) |
| 228 | + const validation = validateOperationArgs(debug_operation, operationArgs) |
| 229 | + //outputChannel.appendLine(`[Debug Tool] Argument validation completed. Valid: ${validation.isValid}.`) |
| 230 | + |
| 231 | + if (!validation.isValid) { |
| 232 | + pushToolResult(formatResponse.toolError(validation.message)) |
| 233 | + return |
| 234 | + } |
| 235 | + |
| 236 | + // Use the transformed arguments from the validation result |
| 237 | + const transformedArgs = validation.transformedArgs |
| 238 | + //outputChannel.appendLine( |
| 239 | + // `[Debug Tool] Using transformed arguments: ${JSON.stringify(transformedArgs, null, 2)}`, |
| 240 | + //) |
| 241 | + |
| 242 | + const targetMethod: DebugOperationFn | undefined = moduleOperationMap[debug_operation] |
| 243 | + |
| 244 | + if (targetMethod) { |
| 245 | + try { |
| 246 | + //outputChannel.appendLine(`[Debug Tool] Executing operation '${debug_operation}'...`) |
| 247 | + |
| 248 | + // Some methods on IDebugController might not expect any arguments. |
| 249 | + // The `transformedArgs` will be an empty object {} if argsXml is undefined or empty. |
| 250 | + // Methods that don't take arguments will simply ignore the empty object. |
| 251 | + const rawResult = await targetMethod(transformedArgs) |
| 252 | + |
| 253 | + // Robustly handle the rawResult |
| 254 | + outputChannel.appendLine( |
| 255 | + `[Debug Tool] Operation '${debug_operation}' completed. Result: ${JSON.stringify(rawResult, null, 2)}`, |
| 256 | + ) |
| 257 | + |
| 258 | + if (typeof rawResult === "object" && rawResult !== null) { |
| 259 | + if (rawResult.success === true) { |
| 260 | + // Standard success case: return the full, formatted rawResult. |
| 261 | + pushToolResult(JSON.stringify(rawResult, null, 2)) |
| 262 | + } else if (rawResult.success === false) { |
| 263 | + // Standard failure case: return the full rawResult, JSON stringified, to provide all details. |
| 264 | + pushToolResult( |
| 265 | + formatResponse.toolError( |
| 266 | + `Debug operation '${debug_operation}' failed. Details: ${JSON.stringify(rawResult, null, 2)}`, |
| 267 | + ), |
| 268 | + ) |
| 269 | + } else { |
| 270 | + // Object, but no boolean 'success' field or unexpected value. |
| 271 | + pushToolResult( |
| 272 | + `Debug operation '${debug_operation}' completed with an unusual result structure: ${JSON.stringify(rawResult, null, 2)}`, |
| 273 | + ) |
| 274 | + } |
| 275 | + } else { |
| 276 | + // Not an object, or null. Highly unexpected. |
| 277 | + pushToolResult( |
| 278 | + `Debug operation '${debug_operation}' completed with an unexpected non-object result: ${String(rawResult)}`, |
| 279 | + ) |
| 280 | + } |
| 281 | + } catch (e) { |
| 282 | + await handleError(`executing debug operation '${debug_operation}'`, e as Error) |
| 283 | + pushToolResult(formatResponse.toolError(`Error during '${debug_operation}': ${(e as Error).message}`)) |
| 284 | + } |
| 285 | + } else { |
| 286 | + cline.consecutiveMistakeCount++ |
| 287 | + // Debug tool errors are tracked separately since "debug" is not a valid ToolName |
| 288 | + // We'll use execute_command as a proxy for error tracking |
| 289 | + cline.recordToolError("execute_command") |
| 290 | + pushToolResult(formatResponse.toolError(`Unknown debug operation: ${debug_operation}`)) |
| 291 | + } |
| 292 | + } catch (error) { |
| 293 | + // Catch errors from parsing argsJson or other unexpected issues |
| 294 | + await handleError(`debug tool general error for operation '${debug_operation}'`, error as Error) |
| 295 | + pushToolResult(formatResponse.toolError(`Unexpected error in debug tool: ${(error as Error).message}`)) |
| 296 | + } |
| 297 | +} |
0 commit comments