diff --git a/src/commands/copilotChatCommand.ts b/src/commands/copilotChatCommand.ts new file mode 100644 index 000000000..fc60efde9 --- /dev/null +++ b/src/commands/copilotChatCommand.ts @@ -0,0 +1,260 @@ +import * as vscode from "vscode"; +import { Logs } from "../models/logs"; +import { commands } from "../utils/common/commands"; +import { constants } from "../utils/common/constants"; +import { AstResult } from "../models/results"; + +export class CopilotChatCommand { + context: vscode.ExtensionContext; + logs: Logs; + + constructor(context: vscode.ExtensionContext, logs: Logs) { + this.context = context; + this.logs = logs; + } + + /** + * Detects if we're running in Cursor IDE + */ + private isCursorIDE(): boolean { + try { + // Check if the application name contains "Cursor" + const appName = vscode.env.appName || ''; + if (appName.toLowerCase().includes('cursor')) { + return true; + } + + // Alternative check: try to see if Cursor-specific extensions are installed + const cursorExtensions = vscode.extensions.all.filter(ext => + ext.id.toLowerCase().includes('cursor') || + (ext.packageJSON?.publisher?.toLowerCase()?.includes('cursor')) + ); + + return cursorExtensions.length > 0; + } catch (err) { + this.logs.error(`Error detecting IDE type: ${err}`); + return false; // Default to VS Code if detection fails + } + } + + /** + * Handles opening chat and sending a question in Cursor IDE + * @param question The question to send to Cursor AI + * @returns True if successful, false if all methods failed + */ + private async handleCursorIDE(question: string): Promise { + try { + this.logs.info("Handling Cursor IDE integration"); + + // Copy the question to clipboard as a fallback right away + await vscode.env.clipboard.writeText(question); + const formattedQuestion = { + text: question, + from: 'user' + }; + "editor.action.en" + + // Step 1: Try to open the Cursor chat with composer.startComposerPrompt + this.logs.info("Opening Cursor chat with composer.startComposerPrompt"); + try { + // Try to open the chat with the question + // await vscode.commands.executeCommand("composer.startComposerPrompt"); + await vscode.commands.executeCommand("composer.newAgentChat", "massage to chat ai"); + await vscode.commands.executeCommand("composer.fixerrormessage", "Fix this problem"); + try { + await vscode.commands.executeCommand("editor.action.clipboardPasteAction"); + this.logs.info("Programmatically pasted content from clipboard"); + vscode.commands.executeCommand('composer.sendToAgent'); + vscode.commands.executeCommand('list.select'); + vscode.commands.executeCommand('list.toggleExpand'); + vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + + + vscode.window.showInformationMessage("Question pasted into Cursor chat"); + await vscode.commands.executeCommand('workbench.action.submitComment'); + vscode.commands.executeCommand('type', { text: '\n' }); + } catch (pasteErr) { + this.logs.error(`Failed to programmatically paste: ${pasteErr}`); + vscode.window.showInformationMessage("Question copied to clipboard. Please paste it into the Cursor chat."); + } + + // await vscode.commands.executeCommand(constants.cursorComposerMessage, question) + + this.logs.info("Successfully opened Cursor Composer with question"); + // Step 2: Try to send the question using composer.sendToAgent + try { + // Wait a moment to ensure the chat is ready + await new Promise(resolve => setTimeout(resolve, 10000)); + this.logs.info("Sending question to Cursor chat with composer.sendToAgent"); + + // Format the question as an object with the proper structure + // const formattedQuestion = { + // text: question, + // from: 'user' + // }; + + await vscode.commands.executeCommand(constants.cursorComposerSender, formattedQuestion); + this.logs.info("Successfully sent question to Cursor chat"); + vscode.window.showInformationMessage("Query sent to Cursor AI"); + return true; + } catch (sendErr) { + this.logs.error(`Failed to send question to Cursor chat: ${sendErr}`); + // Chat is open but sending failed - inform user to paste from clipboard + vscode.window.showInformationMessage("Question copied to clipboard. Please paste it into the Cursor chat."); + return true; // Still return true as we successfully opened the chat + } + } catch (openErr) { + this.logs.error(`Failed to open Cursor chat: ${openErr}`); + vscode.window.showInformationMessage("Could not open Cursor chat. Question copied to clipboard."); + return false; + } + } catch (err) { + this.logs.error(`Error in Cursor IDE integration: ${err}`); + vscode.window.showInformationMessage("Question copied to clipboard. Please open Cursor Chat manually."); + return false; + } + } + + public registerCopilotChatCommand() { + this.context.subscriptions.push( + vscode.commands.registerCommand(commands.openCopilotChat, async (item: any) => { + try { + // Log the action + this.logs.info("Opening Copilot Chat to ask about vulnerability"); + + // Debug: Log information about the item + this.logs.info(`Item type: ${typeof item}`); + this.logs.info(`Item properties: ${Object.keys(item).join(', ')}`); + this.logs.info(`Item contextValue: ${item.contextValue}`); + this.logs.info(`Has result property: ${item.result !== undefined}`); + + // Get vulnerability details from the selected item + const result = item?.result as AstResult; + if (!result) { + vscode.window.showErrorMessage("No vulnerability details found. Please select a valid vulnerability."); + return; + } + + // Debug: Log information about the result + this.logs.info(`Result type: ${typeof result}`); + this.logs.info(`Result properties: ${Object.keys(result).join(', ')}`); + + // Extract relevant information from the vulnerability + const vulnName = result.queryName || result.label || "security vulnerability"; + const severity = result.severity || "High"; + const language = result.language || ""; + const category = result.type || ""; + const fileName = result.fileName || ""; + const description = result.description || ""; + + // Construct a more detailed message to send to Copilot + const question = `Can you explain this ${severity} severity security vulnerability: "${vulnName}" detected in ${language} code? +It was found in file ${fileName} and categorized as ${category}. +${description ? `Additional description: ${description}` : ''} +How can I fix it in my code?`; + + // Show info message to user + vscode.window.showInformationMessage("Opening Copilot Chat to ask about this vulnerability"); + + // Check if Copilot Chat is available + try { + // Try to find the Copilot Chat extension + const copilotChatExtension = vscode.extensions.getExtension(constants.copilotChatExtensionId); + if (!copilotChatExtension) { + // Copilot Chat not installed - show installation message + const installOption = "Install Copilot Chat"; + const choice = await vscode.window.showErrorMessage( + "GitHub Copilot Chat extension is not installed. Install it to use this feature.", + installOption + ); + + if (choice === installOption) { + // Open the extension in marketplace + await vscode.commands.executeCommand('workbench.extensions.search', `@id:${constants.copilotChatExtensionId}`); + } + return; + } + + // Detect if we're running in Cursor IDE + const isCursorIDE = this.isCursorIDE(); + this.logs.info(`Detected environment: ${isCursorIDE ? 'Cursor IDE' : 'VS Code'}`); + if (isCursorIDE) { + // Try Cursor IDE specific commands + const cursorSuccess = await this.handleCursorIDE(question); + if (cursorSuccess) { + return; + } else { + vscode.window.showInformationMessage("Question copied to clipboard. Please open Cursor Chat and paste it."); + return; + } + } + + // If not in Cursor, try to open Copilot Chat with the modern approach (direct query) + try { + // Try the modern command that opens chat with a query directly + this.logs.info("Trying modern Copilot Chat opening approach"); + await vscode.commands.executeCommand( + constants.copilotNewChatOpenWithQueryCommand); + // Wait for the chat to open + await vscode.commands.executeCommand( + constants.copilotChatOpenWithQueryCommand, + { query: `@copilot ${question}` } + ); + return; // If successful, exit the function + } catch (err) { + // If the modern approach fails, fall back to the older methods + this.logs.info(`Modern Copilot Chat command failed: ${err}. Trying older methods.`); + + // Try older methods to open Copilot Chat + try { + // First try the standard command + await vscode.commands.executeCommand(constants.copilotShowCommand); + + // Use a slight delay to ensure the chat window is ready + setTimeout(async () => { + try { + // Send the question to Copilot + await vscode.commands.executeCommand(constants.copilotSendRequestCommand, question); + } catch (err) { + // If sending fails, automatically copy to clipboard + await vscode.env.clipboard.writeText(question); + vscode.window.showInformationMessage("Question copied to clipboard. Please paste it into Copilot Chat."); + } + }, 1000); + } catch (err) { + // If the standard command fails, try alternative commands + this.logs.info(`First Copilot command failed: ${err}. Trying alternative.`); + try { + // Try alternative command (Copilot might use different command in some versions) + await vscode.commands.executeCommand(constants.copilotFocusCommand); + + // Use a slight delay to ensure the chat window is ready + setTimeout(async () => { + try { + await vscode.commands.executeCommand(constants.copilotSendRequestCommand, question); + } catch (e) { + // If sending the question fails, automatically copy and notify the user + await vscode.env.clipboard.writeText(question); + vscode.window.showInformationMessage("Question copied to clipboard. Please paste it into Copilot Chat."); + } + }, 1000); + } catch (e) { + // If all commands fail, automatically copy the question to clipboard + this.logs.error(`Could not open Copilot Chat with any command: ${e}`); + await vscode.env.clipboard.writeText(question); + vscode.window.showInformationMessage("Question copied to clipboard. Please open Copilot Chat and paste it."); + } + } + } + } catch (err) { + this.logs.error(`Error interacting with Copilot Chat: ${err}`); + vscode.window.showErrorMessage(`Copilot Chat interaction error: ${err.message}`); + } + } catch (error) { + this.logs.error(`Error opening Copilot Chat: ${error}`); + vscode.window.showErrorMessage(`Failed to open Copilot Chat: ${error}`); + } + }) + ); + } +} diff --git a/src/commands/debugCommand.ts b/src/commands/debugCommand.ts new file mode 100644 index 000000000..9ea5cf817 --- /dev/null +++ b/src/commands/debugCommand.ts @@ -0,0 +1,45 @@ +import * as vscode from "vscode"; +import { Logs } from "../models/logs"; +import { constants } from "../utils/common/constants"; + +export class DebugCommand { + context: vscode.ExtensionContext; + logs: Logs; + + constructor(context: vscode.ExtensionContext, logs: Logs) { + this.context = context; + this.logs = logs; + } + + public registerDebugCommand() { + // Register a command to log information about tree items + this.context.subscriptions.push( + vscode.commands.registerCommand(`${constants.extensionName}.debugTreeItem`, async (item: any) => { + try { + this.logs.info("Debug tree item called"); + this.logs.info(`Item type: ${typeof item}`); + + if (item) { + this.logs.info(`Item properties: ${Object.keys(item).join(', ')}`); + this.logs.info(`Item contextValue: ${item.contextValue}`); + this.logs.info(`Item label: ${item.label}`); + this.logs.info(`Has result property: ${item.result !== undefined}`); + + if (item.result) { + this.logs.info(`Result type: ${typeof item.result}`); + this.logs.info(`Result properties: ${Object.keys(item.result).join(', ')}`); + this.logs.info(`Result severity: ${item.result.severity}`); + this.logs.info(`Result label: ${item.result.label}`); + } + } else { + this.logs.info("Item is undefined"); + } + + vscode.window.showInformationMessage("Debug info logged to output channel"); + } catch (error) { + this.logs.error(`Error in debug command: ${error}`); + } + }) + ); + } +} diff --git a/src/extension.ts b/src/extension.ts index 19ffc57db..c06abf0df 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,6 +29,8 @@ import { AuthService } from "./services/authService"; import { initialize } from "./cx"; let globalContext: vscode.ExtensionContext; +import { CopilotChatCommand } from "./commands/copilotChatCommand"; +import { DebugCommand } from "./commands/debugCommand"; export async function activate(context: vscode.ExtensionContext) { // Initialize cx first @@ -221,6 +223,15 @@ export async function activate(context: vscode.ExtensionContext) { // Register pickers command const pickerCommand = new PickerCommand(context, logs, astResultsProvider); pickerCommand.registerPickerCommands(); + + // Register Copilot Chat integration command + const copilotChatCommand = new CopilotChatCommand(context, logs); + copilotChatCommand.registerCopilotChatCommand(); + + // Register debug command for troubleshooting + const debugCommand = new DebugCommand(context, logs); + debugCommand.registerDebugCommand(); + // Visual feedback on wrapper errors commonCommand.registerErrors(); // Registe Kics remediation command @@ -236,16 +247,16 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - + vscode.commands.registerCommand("ast-results.mockTokenTest", async () => { const authService = AuthService.getInstance(context); await authService.saveToken(context, "FAKE_TOKEN_FROM_TEST"); - console.log(">> Mock token has been saved to secrets"); + console.log(">> Mock token has been saved to secrets"); await authService.validateAndUpdateState(); }); - + } export function getGlobalContext(): vscode.ExtensionContext { diff --git a/src/utils/common/commands.ts b/src/utils/common/commands.ts index 73f98e2c2..49e2c1571 100644 --- a/src/utils/common/commands.ts +++ b/src/utils/common/commands.ts @@ -14,6 +14,8 @@ export const commands = { isScanEnabled: `${constants.extensionName}.isScanEnabled`, isScaScanEnabled: `${constants.extensionName}.isSCAScanEnabled`, + openCopilotChat: `${constants.extensionName}.openCopilotChat`, + filterCriticalToggle: `${constants.extensionName}.filterCritical_toggle`, filterCriticalUntoggle: `${constants.extensionName}.filterCritical_untoggle`, filterCritical: `${constants.extensionName}.filterCritical`, diff --git a/src/utils/common/constants.ts b/src/utils/common/constants.ts index 2f5834a77..9c058388a 100644 --- a/src/utils/common/constants.ts +++ b/src/utils/common/constants.ts @@ -50,6 +50,7 @@ export const constants = { requestChangesItem: "requestChanges-item", mailItem: "mail-item", calendarItem: "calendar-item", + vulnerabilityItem: "vulnerability-item", resultsFileName: "ast-results", resultsFileExtension: "json", status: [ @@ -159,12 +160,28 @@ export const constants = { ascaEngineName: "ASCA", ActivateAscaAutoScanning: "Activate ASCA", CheckmarxAsca: "Checkmarx AI Secure Coding Assistant (ASCA)", - criticalSeverity: "CRITICAL", highSeverity: "HIGH", mediumSeverity: "MEDIUM", lowSeverity: "LOW", - infoSeverity: "INFO", + infoSeverity: "INFO", // Copilot integration + copilotChatExtensionId: "GitHub.copilot-chat", + copilotShowCommand: "github.copilot.chat.show", + copilotFocusCommand: "github.copilot.chat.focus", + copilotSendRequestCommand: "github.copilot.chat.sendRequest", + // Modern way to open Copilot Chat with a query + copilotNewChatOpenWithQueryCommand: "workbench.action.chat.newChat", + copilotChatOpenWithQueryCommand: "workbench.action.chat.open", + // Cursor IDE specific commands + cursorChatOpenCommand: "cursor.openChat", + cursorChatSendCommand: "cursor.chat.submit", + cursorComposerCommand: "composer.startComposerPrompt", + cursorComposerMessage: "composer.fixerrormessage", + // Alternative ways to open Cursor chat + cursorComposerSender: "composer.sendToAgent", + // cursorComposerSender: "notebook.cell.chat.accept", + + cursorChatAlternativeCommand: "workbench.action.terminal.sendSequence", }; export enum GroupBy {