Skip to main content
Glama
nrwl

Nx MCP Server

Official
by nrwl
nx-cloud-fix-webview.ts20.2 kB
import { downloadAndExtractArtifact, nxCloudAuthHeaders, } from '@nx-console/shared-nx-cloud'; import { CIPEInfo, CIPERunGroup, NxCloudFixDetails, NxCloudFixMessage, } from '@nx-console/shared-types'; import { getNxWorkspacePath } from '@nx-console/vscode-configuration'; import { getNxCloudStatus } from '@nx-console/vscode-nx-workspace'; import { logAndShowError, vscodeLogger, } from '@nx-console/vscode-output-channels'; import { getTelemetry } from '@nx-console/vscode-telemetry'; import { applyFixLocallyWithGit, applyFixLocallyWithNxCloud, } from './apply-fix-locally'; import { getGitApi, getGitBranch, getGitHasUncommittedChanges, getWorkspacePath, } from '@nx-console/vscode-utils'; import { execSync } from 'child_process'; import { join } from 'path'; import { commands, EventEmitter, ExtensionContext, Tab, Uri, ViewColumn, WebviewPanel, window, workspace, } from 'vscode'; import { ActorRef, EventObject } from 'xstate'; import { getAiFixStatusBarService } from './ai-fix-status-bar-service'; import { DiffContentProvider, parseGitDiff } from './diffs/diff-provider'; import { createUnifiedDiffView } from './nx-cloud-fix-tree-item'; import { httpRequest } from '@nx-console/shared-utils'; export class NxCloudFixWebview { private webviewPanel: WebviewPanel | undefined; private readonly _onDispose = new EventEmitter<void>(); private currentFixDetails: NxCloudFixDetails | undefined; constructor(private context: ExtensionContext) {} get onDispose() { return this._onDispose.event; } async showFixDetails(details: NxCloudFixDetails) { this.currentFixDetails = details; if (!this.webviewPanel) { this.createWebviewPanel(); } this.updateWebviewContent(); this.webviewPanel?.reveal(); // Open the diff view in a split panel only if the fix is ready if (details.runGroup.aiFix?.suggestedFix) { await this.showDiffInSplitPanel(details.runGroup.aiFix.suggestedFix); } } async updateFixDetailsFromRecentCIPEs(recentCIPEs: CIPEInfo[]) { if (!this.currentFixDetails) return; const updatedDetails = recentCIPEs.find( (cipe) => cipe.ciPipelineExecutionId === this.currentFixDetails.cipe.ciPipelineExecutionId, ); if (updatedDetails) { // Find the corresponding runGroup in the updated CIPE const updatedRunGroup = updatedDetails.runGroups.find( (rg) => rg.runGroup === this.currentFixDetails.runGroup.runGroup, ); if (updatedRunGroup) { this.currentFixDetails = { ...this.currentFixDetails, cipe: updatedDetails, runGroup: updatedRunGroup, }; } } if (this.webviewPanel) { this.updateWebviewContent(); } } private createWebviewPanel() { this.webviewPanel = window.createWebviewPanel( 'nxCloudFix', 'Nx Cloud Fix Details', ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [this.context.extensionUri], }, ); this.webviewPanel.webview.onDidReceiveMessage( async (message: NxCloudFixMessage) => { await this.handleWebviewMessage(message); }, ); this.webviewPanel.onDidDispose(async () => { this.webviewPanel = undefined; this.currentFixDetails = undefined; this._onDispose.fire(); // Close the diff tab associated with this fix await closeCloudFixDiffTab(); }); this.webviewPanel.webview.html = this.getWebviewHtml( this.currentFixDetails, ); } private async handleWebviewMessage(message: NxCloudFixMessage) { if (!this.currentFixDetails) return; switch (message.type) { case 'apply': await commands.executeCommand( 'nxCloud.applyAiFix', this.currentFixDetails, message.commitMessage, ); this.webviewPanel?.dispose(); break; case 'reject': await commands.executeCommand( 'nxCloud.rejectAiFix', this.currentFixDetails, ); this.webviewPanel?.dispose(); break; case 'show-diff': if (this.currentFixDetails.runGroup.aiFix?.suggestedFix) { await this.showDiffInSplitPanel( this.currentFixDetails.runGroup.aiFix.suggestedFix, ); } break; case 'apply-locally': await commands.executeCommand( 'nxCloud.applyAiFixLocally', this.currentFixDetails, ); this.webviewPanel?.dispose(); break; } } async updateWebviewContent() { if (!this.webviewPanel || !this.currentFixDetails) return; const hasUncommittedChanges = await getGitHasUncommittedChanges(); this.webviewPanel.webview.postMessage({ type: 'update-details', details: { ...this.currentFixDetails, hasUncommittedChanges, }, }); } private getWebviewHtml(details: NxCloudFixDetails): string { const webviewScriptUri = this.webviewPanel?.webview.asWebviewUri( Uri.joinPath(this.context.extensionUri, 'cloud-fix-webview', 'main.js'), ); const codiconsUri = this.webviewPanel?.webview.asWebviewUri( Uri.joinPath( this.context.extensionUri, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.css', ), ); const tailwindCssUri = this.webviewPanel?.webview.asWebviewUri( Uri.joinPath( this.context.extensionUri, 'cloud-fix-webview', 'tailwind.css', ), ); const mainCssUri = this.webviewPanel?.webview.asWebviewUri( Uri.joinPath(this.context.extensionUri, 'cloud-fix-webview', 'main.css'), ); const vscodeElementsUri = this.webviewPanel?.webview.asWebviewUri( Uri.joinPath( this.context.extensionUri, 'node_modules', '@vscode-elements', 'elements', 'dist', 'bundled.js', ), ); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="${codiconsUri}" rel="stylesheet" id="vscode-codicon-stylesheet"> <link href="${mainCssUri}" rel="stylesheet"> <link href="${tailwindCssUri}" rel="stylesheet"> <style> :root { font-size: var(--vscode-font-size); } body { padding: 0; } </style> <title>Nx Cloud Fix Details</title> <script src="${vscodeElementsUri}" type="module"></script> </head> <body> <script type="module" src="${webviewScriptUri}"></script> <script> globalThis.fixDetails = ${JSON.stringify(details)}; </script> <root-nx-cloud-fix-element></root-nx-cloud-fix-element> </body> </html>`; } private async showDiffInSplitPanel(gitDiff: string) { // Parse the git diff to extract file changes const parsedDiff = parseGitDiff(gitDiff); if (parsedDiff.length === 0) { // If we can't parse the diff, fall back to showing raw diff const doc = await workspace.openTextDocument({ content: gitDiff, language: 'diff', }); await window.showTextDocument(doc, { viewColumn: ViewColumn.Beside, preview: false, }); return; } // Try to use VS Code's MultiDiffEditor via the vscode.changes command const timestamp = Date.now(); const changeUris: [Uri, Uri, Uri][] = []; // Get the workspace path for constructing real file URIs const workspacePath = getWorkspacePath(); if (!workspacePath) { return; } for (const fileDiff of parsedDiff) { // Create unique identifiers for the virtual content const fileId = `${fileDiff.fileName.replace(/[^a-zA-Z0-9]/g, '_')}_${timestamp}`; // Create virtual URIs for before and after content with clean paths const beforeUri = Uri.parse(`nx-cloud-fix-before:${fileId}`).with({ path: `${fileDiff.fileName}`, query: `before-${timestamp}`, }); const afterUri = Uri.parse(`nx-cloud-fix-after:${fileId}`).with({ path: `${fileDiff.fileName}`, query: `after-${timestamp}`, }); // Store content in the provider DiffContentProvider.setContent( beforeUri.toString(), fileDiff.beforeContent, ); DiffContentProvider.setContent( afterUri.toString(), fileDiff.afterContent, ); // Create the resource URI that points to the REAL workspace file const absoluteFilePath = join(workspacePath, fileDiff.fileName); const resourceUri = Uri.file(absoluteFilePath); // Add to the changes array: [resourceUri, originalUri, modifiedUri] changeUris.push([resourceUri, beforeUri, afterUri]); } // Try to open with the MultiDiffEditor command in the beside column try { const title = `Nx Cloud Fix (${parsedDiff.length} file${parsedDiff.length === 1 ? '' : 's'})`; // First focus the beside column await commands.executeCommand('workbench.action.focusSecondEditorGroup'); // Then open the multi-diff in that column await commands.executeCommand('vscode.changes', title, changeUris); console.log('MultiDiffEditor opened successfully'); } catch (error) { // If the command doesn't exist or fails, fall back to unified diff console.log( 'MultiDiffEditor not available, falling back to unified diff', error, ); // Create a unified diff view showing all files const unifiedDiff = createUnifiedDiffView(parsedDiff); // Create virtual URIs for the unified before and after content const beforeUri = Uri.parse( `nx-cloud-fix-before:unified_${timestamp}/Nx Cloud Fix (Before)`, ); const afterUri = Uri.parse( `nx-cloud-fix-after:unified_${timestamp}/Nx Cloud Fix (After)`, ); // Store the unified content in the provider DiffContentProvider.setContent( beforeUri.toString(), unifiedDiff.beforeContent, ); DiffContentProvider.setContent( afterUri.toString(), unifiedDiff.afterContent, ); // Show the unified diff in the beside column const title = `Nx Cloud Fix (${parsedDiff.length} file${parsedDiff.length === 1 ? '' : 's'})`; await commands.executeCommand('vscode.diff', beforeUri, afterUri, title, { preview: false, preserveFocus: false, viewColumn: ViewColumn.Beside, }); } } static create( extensionContext: ExtensionContext, actor: ActorRef<any, EventObject>, ): NxCloudFixWebview { const nxCloudFixWebview = new NxCloudFixWebview(extensionContext); const diffContentProvider = new DiffContentProvider(); extensionContext.subscriptions.push( workspace.registerTextDocumentContentProvider( 'nx-cloud-fix-before', diffContentProvider, ), workspace.registerTextDocumentContentProvider( 'nx-cloud-fix-after', diffContentProvider, ), ); const subscription = actor.subscribe((state) => { const recentCIPEs = state.context.recentCIPEs; if (recentCIPEs) { nxCloudFixWebview.updateFixDetailsFromRecentCIPEs(recentCIPEs); } }); // listen to git branch changes const repo = getGitApi().getRepository(Uri.file(getWorkspacePath())); if (repo) { extensionContext.subscriptions.push( repo.state.onDidChange(async () => { nxCloudFixWebview.updateWebviewContent(); }), ); } extensionContext.subscriptions.push({ dispose: () => { subscription.unsubscribe(); }, }); extensionContext.subscriptions.push( commands.registerCommand( 'nxCloud.applyAiFix', async ( data: { cipe: CIPEInfo; runGroup: CIPERunGroup }, commitMessage?: string, ) => { getTelemetry().logUsage('cloud.apply-ai-fix'); if (!data.runGroup.aiFix?.suggestedFix) { window.showErrorMessage('No AI fix available to apply'); return; } const aiFixId = data.runGroup.aiFix.aiFixId; if (!aiFixId) { window.showErrorMessage('AI fix ID not found'); return; } const success = await updateSuggestedFix( aiFixId, 'APPLIED', commitMessage, ); if (success) { getAiFixStatusBarService().hideAiFixStatusBarItem(); } }, ), commands.registerCommand( 'nxCloud.applyAiFixLocally', async (data: { cipe: CIPEInfo; runGroup: CIPERunGroup }) => { getTelemetry().logUsage('cloud.apply-ai-fix-locally'); if (!data.runGroup.aiFix?.suggestedFix) { window.showErrorMessage('No AI fix available to apply locally'); return; } const branch = await getGitBranch(); if (branch && branch !== data.cipe.branch) { const result = await window.showWarningMessage( 'Are you sure you want to apply the fix locally?', { modal: true, detail: `Your local branch ${branch} does not match the branch ${data.cipe.branch} of the CI pipeline execution.`, }, 'Apply', ); if (result !== 'Apply') { return; } } try { if (data.runGroup.aiFix.shortLinkId) { await applyFixLocallyWithNxCloud(data.runGroup.aiFix.shortLinkId); } else { await applyFixLocallyWithGit(data.runGroup.aiFix.suggestedFix); await updateSuggestedFix( data.runGroup.aiFix.aiFixId, 'APPLIED_LOCALLY', ); } getAiFixStatusBarService().hideAiFixStatusBarItem(); } catch (error) { vscodeLogger.log( `Failed to apply Nx Cloud fix locally: ${error.stderr || error.message}`, ); window.showErrorMessage( 'Failed to apply Nx Cloud fix locally. Please check the output for more details.', ); return; } }, ), commands.registerCommand( 'nxCloud.rejectAiFix', async (data: { cipe: CIPEInfo; runGroup: CIPERunGroup }) => { getTelemetry().logUsage('cloud.reject-ai-fix'); if (!data.runGroup.aiFix) { window.showErrorMessage('No AI fix available to ignore'); return; } const aiFixId = data.runGroup.aiFix.aiFixId; if (!aiFixId) { window.showErrorMessage('AI fix ID not found'); return; } const success = await updateSuggestedFix(aiFixId, 'REJECTED'); if (success) { window.showInformationMessage('Nx Cloud fix ignored'); commands.executeCommand('nxCloud.refresh'); getAiFixStatusBarService().hideAiFixStatusBarItem(); } }, ), commands.registerCommand( 'nxCloud.openFixDetails', async (args: { cipeId: string; runGroupId: string }) => { const recentCIPEs = actor.getSnapshot().context.recentCIPEs; // Find the parent CIPE const cipe = recentCIPEs?.find( (c: CIPEInfo) => c.ciPipelineExecutionId === args.cipeId, ); const runGroup = cipe?.runGroups.find( (rg: CIPERunGroup) => rg.runGroup === args.runGroupId, ); if (!cipe) { vscodeLogger.log(`CIPE ${args.cipeId} not found`); return; } else if (!runGroup) { vscodeLogger.log( `Run group ${args.runGroupId} not found in CIPE ${args.cipeId}`, ); return; } if (!runGroup.aiFix) { vscodeLogger.log('No AI fix available on tree item'); return; } console.log('Found CIPE, calling webview.showFixDetails'); getTelemetry().logUsage('cloud.open-fix-details', { source: 'cloud-view', }); let terminalOutput: string | undefined; const failedTaskId = runGroup.aiFix.taskIds[0]; try { const terminalOutputUrl = runGroup.aiFix.terminalLogsUrls[failedTaskId]; terminalOutput = await downloadAndExtractArtifact( terminalOutputUrl, vscodeLogger, ); } catch (error) { vscodeLogger.log( `Failed to retrieve terminal output for task ${failedTaskId}: ${error}`, ); terminalOutput = 'Failed to retrieve terminal output. Please check the Nx Console output for more details.'; } await nxCloudFixWebview.showFixDetails({ cipe, runGroup: runGroup, terminalOutput, }); getAiFixStatusBarService().hideAiFixStatusBarItem(); }, ), ); return nxCloudFixWebview; } } async function updateSuggestedFix( aiFixId: string, action: 'APPLIED' | 'REJECTED' | 'APPLIED_LOCALLY', commitMessage?: string, ): Promise<boolean> { try { const nxCloudInfo = await getNxCloudStatus(); if (!nxCloudInfo?.nxCloudUrl) { window.showErrorMessage('Nx Cloud URL not found'); return false; } const workspacePath = getWorkspacePath(); const requestData: any = { aiFixId, action, actionOrigin: 'NX_CONSOLE_VSCODE', }; // Only include userCommitMessage if it was provided if (commitMessage) { requestData.userCommitMessage = commitMessage; } const response = await httpRequest({ url: `${nxCloudInfo.nxCloudUrl}/nx-cloud/update-suggested-fix`, type: 'POST', headers: { 'Content-Type': 'application/json', ...(await nxCloudAuthHeaders(workspacePath)), }, data: JSON.stringify(requestData), }); if (response.status >= 200 && response.status < 300) { return true; } else { throw new Error(`HTTP ${response.status}: ${response.responseText}`); } } catch (error) { console.error('Failed to update suggested fix:', error); window.showErrorMessage( `Failed to ${action.toLowerCase()} AI fix: ${error.responseText ?? error.message ?? 'Unknown error'}`, ); return false; } } export async function closeCloudFixDiffTab() { const diffTab = window.tabGroups.all .flatMap((g) => g.tabs) .find((t) => isCloudFixTab(t)); if (diffTab) { await window.tabGroups.close(diffTab, true); } } function isCloudFixTab(tab: Tab): boolean { return ( (tab as any)?.input?.textDiffs?.[0]?.original?.scheme === 'nx-cloud-fix-before' ); } /** * Fetch and pull changes from remote after a fix has been applied * @param targetBranch The branch to fetch and pull from */ export async function fetchAndPullChanges(targetBranch: string): Promise<void> { try { const cwd = getNxWorkspacePath(); // Always refresh remotes first execSync('git fetch origin', { cwd }); // Get current branch name const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8', }).trim(); if (currentBranch === targetBranch) { // On target branch: fast-forward your working tree execSync(`git pull --ff-only origin ${targetBranch}`, { cwd, }); } else { // On another branch: fast-forward local target branch without checking it out // This creates the branch if missing, refuses if it wouldn't be a fast-forward execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { cwd }); } } catch (e) { logAndShowError( 'Failed to fetch and pull changes. Please check the output and try again yourself.', `Failed to fetch and pull changes: ${e.stderr?.toString() || e.message}`, ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nrwl/nx-console'

If you have feedback or need assistance with the MCP directory API, please join our Discord server