From 8da01bed049573398b9bbadc8c21687f2ba4ef3e Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Mon, 22 Sep 2025 17:14:18 +0200 Subject: [PATCH] CM-53313 - Add "Apply AI suggested fix" with the state tracker --- src/ui/panels/violation/js.ts | 53 +++++++++++-- src/ui/panels/violation/violation-panel.ts | 88 +++++++++++++++++----- 2 files changed, 116 insertions(+), 25 deletions(-) diff --git a/src/ui/panels/violation/js.ts b/src/ui/panels/violation/js.ts index d933292..6bf73a7 100644 --- a/src/ui/panels/violation/js.ts +++ b/src/ui/panels/violation/js.ts @@ -69,19 +69,20 @@ export default (detectionType: CliScanType) => ` const resetAiElements = () => { if (isAiEnabled) { showElement('ai-remediation-btn'); + resetButton('ai-remediation-btn'); } else { hideElement('ai-remediation-btn'); } hideElement('ai-apply-fix-btn'); + resetButton('ai-apply-fix-btn'); + hideElement('ai-remediation'); ge('ai-remediation-text').innerText = 'None'; ge('ai-remediation-diff').innerText = ''; } const renderAiRemediation = (remediation, unifyDiff, isFixAvailable) => { - isFixAvailable = false; // disable fix for now; not ready for production - hideElement('ai-remediation-btn'); ge('ai-remediation-text').innerHTML = remediation; showElement('ai-remediation'); @@ -113,10 +114,52 @@ export default (detectionType: CliScanType) => ` showElement('ai-remediation-diff'); }; - const registerAiButtonCallbacks = () => { - ge('ai-remediation-btn').onclick = () => { - vscode.postMessage({ command: 'getAiRemediation', uniqueDetectionId }); + const resetButton = (buttonClassName) => { + const button = ge(buttonClassName); + if (button && button._originalText !== undefined) { + button.disabled = button._originalDisabled; + button.innerText = button._originalText; + } + }; + + const registerButton = (buttonClassName, commandId, inProgressText, additionalData = {}) => { + const button = ge(buttonClassName); + if (!button) return; + + // Store the original state in private fields for later restoration + button._originalText = button.innerText; + button._originalDisabled = button.disabled; + + button.onclick = () => { + button.disabled = true; + if (inProgressText !== undefined) { + button.innerText = inProgressText; + } + + const messageData = { command: commandId, ...additionalData }; + if (uniqueDetectionId) { + messageData.uniqueDetectionId = uniqueDetectionId; + } + + vscode.postMessage(messageData); }; + + const messageHandler = event => { + if (event.data.command === commandId && event.data.finished !== undefined) { + button.disabled = button._originalDisabled; + button.innerText = button._originalText; + } + }; + window.addEventListener('message', messageHandler); + + button._cleanupHandler = () => { + window.removeEventListener('message', messageHandler); + }; + }; + + const registerAiButtonCallbacks = () => { + registerButton('ai-remediation-btn', 'getAiRemediation', 'Generating...'); + registerButton('ai-apply-fix-btn', 'applyAiSuggestedFix', 'Applying...'); } ${detectionType === CliScanType.Sca ? scaRenderer : ''} diff --git a/src/ui/panels/violation/violation-panel.ts b/src/ui/panels/violation/violation-panel.ts index 232ace4..4419a15 100644 --- a/src/ui/panels/violation/violation-panel.ts +++ b/src/ui/panels/violation/violation-panel.ts @@ -135,39 +135,85 @@ const _extractAiRemediationParts = (remediation: string) => { return undefined; }; +const _sendCommandCompletionMessage = async (panel: vscode.WebviewPanel, command: string, success: boolean) => { + const logger = container.resolve(LoggerServiceSymbol); + const sendRes = await panel.webview.postMessage({ + command: command, + finished: success, + }); + if (!sendRes) { + logger.error(`Failed to send command completion message for ${command}`); + } +}; + const _getAiRemediationHandler = async (panel: vscode.WebviewPanel, uniqueDetectionId: string) => { const scanResultsService = container.resolve(ScanResultsServiceSymbol); const detection = scanResultsService.getDetectionById(uniqueDetectionId); if (!detection) { + await _sendCommandCompletionMessage(panel, 'getAiRemediation', false); return; } const cycodeService = container.resolve(CycodeServiceSymbol); const logger = container.resolve(LoggerServiceSymbol); - const aiRemediation = await cycodeService.getAiRemediation(detection.id); - if (!aiRemediation) { - logger.error('Failed to get AI remediation'); - return; - } - let remediationMarkdown = aiRemediation.remediation; - let unifyDiff = undefined; + try { + const aiRemediation = await cycodeService.getAiRemediation(detection.id); + if (!aiRemediation) { + logger.error('Failed to get AI remediation'); + await _sendCommandCompletionMessage(panel, 'getAiRemediation', false); + return; + } + + let remediationMarkdown = aiRemediation.remediation; + let unifyDiff = undefined; + + const remediationParts = _extractAiRemediationParts(aiRemediation.remediation); + if (remediationParts) { + remediationMarkdown = remediationParts.remediationMarkdown; + unifyDiff = remediationParts.unifyDiff; + } + + const sendRes = await panel.webview.postMessage({ + command: 'getAiRemediation', + finished: true, + aiRemediation: { + remediation: getMarkdownForRender(remediationMarkdown), + unifyDiff: unifyDiff, + isFixAvailable: aiRemediation.isFixAvailable, + }, + }); - const remediationParts = _extractAiRemediationParts(aiRemediation.remediation); - if (remediationParts) { - remediationMarkdown = remediationParts.remediationMarkdown; - unifyDiff = remediationParts.unifyDiff; + if (!sendRes) { + logger.error('Failed to send AI Remediation to render on the violation card'); + } + } catch (error) { + logger.error(`Error in AI remediation handler: ${error}`); + await _sendCommandCompletionMessage(panel, 'getAiRemediation', false); } +}; - const sendRes = await panel.webview.postMessage({ - aiRemediation: { - remediation: getMarkdownForRender(remediationMarkdown), - unifyDiff: unifyDiff, - isFixAvailable: aiRemediation.isFixAvailable, - }, - }); - if (!sendRes) { - logger.error('Failed to send AI Remediation to render on the violation card'); +const _applyAiSuggestedFixHandler = async (panel: vscode.WebviewPanel, uniqueDetectionId: string) => { + const logger = container.resolve(LoggerServiceSymbol); + + try { + /* + * TODO: Implement actual AI fix application when CLI command is ready + * For now, simulate the operation with a delay + */ + logger.debug(`[AI FIX] Start applying AI suggested fix for ${uniqueDetectionId}`); + + // Simulate processing time + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // For now, just log that the fix would be applied + logger.debug(`[AI FIX] Finish applying AI suggested fix for ${uniqueDetectionId}`); + + // Send the success completion message + await _sendCommandCompletionMessage(panel, 'applyAiSuggestedFix', true); + } catch (error) { + logger.error(`Error in AI fix handler: ${error}`); + await _sendCommandCompletionMessage(panel, 'applyAiSuggestedFix', false); } }; @@ -179,6 +225,8 @@ const _getOnDidReceiveMessage = (panel: vscode.WebviewPanel, onLoadResolve: (val if (message.command == 'getAiRemediation') { await _getAiRemediationHandler(panel, message.uniqueDetectionId); + } else if (message.command == 'applyAiSuggestedFix') { + await _applyAiSuggestedFixHandler(panel, message.uniqueDetectionId); } else if (message.command == 'ready') { _readyCommandHandler(onLoadResolve); } else if (message.command.startsWith('ignore')) {