Skip to content

Commit c917133

Browse files
singhAwsblakelaz-amazonRathore25BlakeLazarine
authored
feat(amazonq): adding qCodeReview tool updates for Code Issues panel (#7660)
## Feature ### QCodeReview - QCodeReview tool result will be pushed to Code Issues panel <img width="200" height="200" alt="image" src="https://github.com/user-attachments/assets/bf206096-0637-48f7-877c-18c3e4bc8aac" /> - Issues will have `Explain`, `Fix`, `Ignore`, and `Ignore Similar Issues` options. <img width="200" height="200" alt="image" src="https://github.com/user-attachments/assets/9397aba9-9fed-42fd-9a37-232e02f4caab" /> - Same options will be available even on hover <img width="400" height="200" alt="image" src="https://github.com/user-attachments/assets/c3513f53-7f25-4872-a2fa-1631dd14bc80" /> - Explain will pull the issue in chat and Q will explain the issue to the user - Fix will pull the issue into chat and Q will generate a fix for the issue <img width="200" height="250" alt="image" src="https://github.com/user-attachments/assets/0cdad39e-0bce-4265-a4c3-2dc6b4105d2c" /> <img width="200" height="250" alt="image" src="https://github.com/user-attachments/assets/81e9e5cf-0564-4dcf-9e7c-44e07a9459fd" /> --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Blake Lazarine <[email protected]> Co-authored-by: Nitish Singh <[email protected]> Co-authored-by: BlakeLazarine <[email protected]>
1 parent f219b41 commit c917133

19 files changed

+224
-648
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Explain and Fix for any issue in Code Issues panel will pull the experience into chat. Also no more view details tab."
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "QCodeReview tool will update CodeIssues panel along with quick action - `/review`"
4+
}

packages/amazonq/package.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,17 +442,22 @@
442442
},
443443
{
444444
"command": "aws.amazonq.openSecurityIssuePanel",
445+
"when": "false && view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)",
446+
"group": "inline@4"
447+
},
448+
{
449+
"command": "aws.amazonq.security.explain",
445450
"when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)",
446451
"group": "inline@4"
447452
},
448453
{
449-
"command": "aws.amazonq.security.ignore",
454+
"command": "aws.amazonq.security.generateFix",
450455
"when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)",
451456
"group": "inline@5"
452457
},
453458
{
454-
"command": "aws.amazonq.security.generateFix",
455-
"when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithoutFix",
459+
"command": "aws.amazonq.security.ignore",
460+
"when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)",
456461
"group": "inline@6"
457462
},
458463
{
@@ -535,16 +540,17 @@
535540
"aws.amazonq.submenu.securityIssueMoreActions": [
536541
{
537542
"command": "aws.amazonq.security.explain",
543+
"when": "false",
538544
"group": "1_more@1"
539545
},
540546
{
541547
"command": "aws.amazonq.applySecurityFix",
542-
"when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix",
548+
"when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix",
543549
"group": "1_more@3"
544550
},
545551
{
546552
"command": "aws.amazonq.security.regenerateFix",
547-
"when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix",
553+
"when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix",
548554
"group": "1_more@4"
549555
},
550556
{
@@ -793,6 +799,7 @@
793799
{
794800
"command": "aws.amazonq.security.explain",
795801
"title": "%AWS.command.amazonq.explainIssue%",
802+
"icon": "$(search)",
796803
"enablement": "view == aws.amazonq.SecurityIssuesTree"
797804
},
798805
{

packages/amazonq/src/lsp/chat/commands.ts

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { Commands, globals } from 'aws-core-vscode/shared'
77
import { window } from 'vscode'
88
import { AmazonQChatViewProvider } from './webviewProvider'
99
import { CodeScanIssue } from 'aws-core-vscode/codewhisperer'
10-
import { EditorContextExtractor } from 'aws-core-vscode/codewhispererChat'
10+
import { getLogger } from 'aws-core-vscode/shared'
11+
import * as vscode from 'vscode'
12+
import * as path from 'path'
1113

1214
/**
1315
* TODO: Re-enable these once we can figure out which path they're going to live in
@@ -21,45 +23,24 @@ export function registerCommands(provider: AmazonQChatViewProvider) {
2123
registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', provider),
2224
registerGenericCommand('aws.amazonq.generateUnitTests', 'Generate Tests', provider),
2325

24-
Commands.register('aws.amazonq.explainIssue', async (issue: CodeScanIssue) => {
25-
void focusAmazonQPanel().then(async () => {
26-
const editorContextExtractor = new EditorContextExtractor()
27-
const extractedContext = await editorContextExtractor.extractContextForTrigger('ContextMenu')
28-
const selectedCode =
29-
extractedContext?.activeFileContext?.fileText
30-
?.split('\n')
31-
.slice(issue.startLine, issue.endLine)
32-
.join('\n') ?? ''
33-
34-
// The message that gets sent to the UI
35-
const uiMessage = [
36-
'Explain the ',
37-
issue.title,
38-
' issue in the following code:',
39-
'\n```\n',
40-
selectedCode,
41-
'\n```',
42-
].join('')
43-
44-
// The message that gets sent to the backend
45-
const contextMessage = `Explain the issue "${issue.title}" (${JSON.stringify(
46-
issue
47-
)}) and generate code demonstrating the fix`
48-
49-
void provider.webview?.postMessage({
50-
command: 'sendToPrompt',
51-
params: {
52-
selection: '',
53-
triggerType: 'contextMenu',
54-
prompt: {
55-
prompt: uiMessage, // what gets sent to the user
56-
escapedPrompt: contextMessage, // what gets sent to the backend
57-
},
58-
autoSubmit: true,
59-
},
60-
})
61-
})
62-
}),
26+
Commands.register('aws.amazonq.explainIssue', (issue: CodeScanIssue, filePath: string) =>
27+
handleIssueCommand(
28+
issue,
29+
filePath,
30+
'Explain',
31+
'Provide a small description of the issue. You must not attempt to fix the issue. You should only give a small summary of it to the user.',
32+
provider
33+
)
34+
),
35+
Commands.register('aws.amazonq.generateFix', (issue: CodeScanIssue, filePath: string) =>
36+
handleIssueCommand(
37+
issue,
38+
filePath,
39+
'Fix',
40+
'Generate a fix for the following code issue. You must not explain the issue, just generate and explain the fix. The user should have the option to accept or reject the fix before any code is changed.',
41+
provider
42+
)
43+
),
6344
Commands.register('aws.amazonq.sendToPrompt', (data) => {
6445
const triggerType = getCommandTriggerType(data)
6546
const selection = getSelectedText()
@@ -85,6 +66,58 @@ export function registerCommands(provider: AmazonQChatViewProvider) {
8566
)
8667
}
8768

69+
async function handleIssueCommand(
70+
issue: CodeScanIssue,
71+
filePath: string,
72+
action: string,
73+
contextPrompt: string,
74+
provider: AmazonQChatViewProvider
75+
) {
76+
await focusAmazonQPanel()
77+
78+
if (issue && filePath) {
79+
await openFileWithSelection(issue, filePath)
80+
}
81+
82+
const lineRange = createLineRangeText(issue)
83+
const visibleMessageInChat = `_${action} **${issue.title}** issue in **${path.basename(filePath)}** at \`${lineRange}\`_`
84+
const contextMessage = `${contextPrompt} Code issue - ${JSON.stringify(issue)}`
85+
86+
void provider.webview?.postMessage({
87+
command: 'sendToPrompt',
88+
params: {
89+
selection: '',
90+
triggerType: 'contextMenu',
91+
prompt: {
92+
prompt: visibleMessageInChat,
93+
escapedPrompt: contextMessage,
94+
},
95+
autoSubmit: true,
96+
},
97+
})
98+
}
99+
100+
async function openFileWithSelection(issue: CodeScanIssue, filePath: string) {
101+
try {
102+
const range = new vscode.Range(issue.startLine, 0, issue.endLine, 0)
103+
const doc = await vscode.workspace.openTextDocument(filePath)
104+
await vscode.window.showTextDocument(doc, {
105+
selection: range,
106+
viewColumn: vscode.ViewColumn.One,
107+
preview: true,
108+
})
109+
} catch (e) {
110+
getLogger().error('openFileWithSelection: Failed to open file %s with selection: %O', filePath, e)
111+
void vscode.window.showInformationMessage('Failed to display file with issue.')
112+
}
113+
}
114+
115+
function createLineRangeText(issue: CodeScanIssue): string {
116+
return issue.startLine === issue.endLine - 1
117+
? `[${issue.startLine + 1}]`
118+
: `[${issue.startLine + 1}, ${issue.endLine}]`
119+
}
120+
88121
function getSelectedText(): string {
89122
const editor = window.activeTextEditor
90123
if (editor) {

packages/amazonq/src/lsp/chat/messages.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,16 @@ import { v4 as uuidv4 } from 'uuid'
7171
import * as vscode from 'vscode'
7272
import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient'
7373
import { AmazonQChatViewProvider } from './webviewProvider'
74-
import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer'
74+
import {
75+
AggregatedCodeScanIssue,
76+
AuthUtil,
77+
CodeAnalysisScope,
78+
CodeWhispererSettings,
79+
initSecurityScanRender,
80+
ReferenceLogViewProvider,
81+
SecurityIssueTreeViewProvider,
82+
CodeWhispererConstants,
83+
} from 'aws-core-vscode/codewhisperer'
7584
import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl, isTextEditor } from 'aws-core-vscode/shared'
7685
import {
7786
DefaultAmazonQAppInitContext,
@@ -85,6 +94,7 @@ import { isValidResponseError } from './error'
8594
import { decryptResponse, encryptRequest } from '../encryption'
8695
import { getCursorState } from '../utils'
8796
import { focusAmazonQPanel } from './commands'
97+
import { ChatMessage } from '@aws/language-server-runtimes/server-interface'
8898

8999
export function registerActiveEditorChangeListener(languageClient: LanguageClient) {
90100
let debounceTimer: NodeJS.Timeout | undefined
@@ -299,7 +309,8 @@ export function registerMessageListeners(
299309
encryptionKey,
300310
provider,
301311
chatParams.tabId,
302-
chatDisposable
312+
chatDisposable,
313+
languageClient
303314
)
304315
} catch (e) {
305316
const errorMsg = `Error occurred during chat request: ${e}`
@@ -315,7 +326,8 @@ export function registerMessageListeners(
315326
encryptionKey,
316327
provider,
317328
chatParams.tabId,
318-
chatDisposable
329+
chatDisposable,
330+
languageClient
319331
)
320332
} finally {
321333
chatStreamTokens.delete(chatParams.tabId)
@@ -359,7 +371,8 @@ export function registerMessageListeners(
359371
encryptionKey,
360372
provider,
361373
message.params.tabId,
362-
quickActionDisposable
374+
quickActionDisposable,
375+
languageClient
363376
)
364377
break
365378
}
@@ -612,6 +625,12 @@ async function handlePartialResult<T extends ChatResult>(
612625
) {
613626
const decryptedMessage = await decryptResponse<T>(partialResult, encryptionKey)
614627

628+
// This is to filter out the message containing findings from qCodeReview tool to update CodeIssues panel
629+
decryptedMessage.additionalMessages = decryptedMessage.additionalMessages?.filter(
630+
(message) =>
631+
!(message.messageId !== undefined && message.messageId.endsWith(CodeWhispererConstants.findingsSuffix))
632+
)
633+
615634
if (decryptedMessage.body !== undefined) {
616635
void provider.webview?.postMessage({
617636
command: chatRequestType.method,
@@ -632,10 +651,13 @@ async function handleCompleteResult<T extends ChatResult>(
632651
encryptionKey: Buffer | undefined,
633652
provider: AmazonQChatViewProvider,
634653
tabId: string,
635-
disposable: Disposable
654+
disposable: Disposable,
655+
languageClient: LanguageClient
636656
) {
637657
const decryptedMessage = await decryptResponse<T>(result, encryptionKey)
638658

659+
handleSecurityFindings(decryptedMessage, languageClient)
660+
639661
void provider.webview?.postMessage({
640662
command: chatRequestType.method,
641663
params: decryptedMessage,
@@ -649,6 +671,37 @@ async function handleCompleteResult<T extends ChatResult>(
649671
disposable.dispose()
650672
}
651673

674+
function handleSecurityFindings(
675+
decryptedMessage: { additionalMessages?: ChatMessage[] },
676+
languageClient: LanguageClient
677+
): void {
678+
if (decryptedMessage.additionalMessages === undefined || decryptedMessage.additionalMessages.length === 0) {
679+
return
680+
}
681+
for (let i = decryptedMessage.additionalMessages.length - 1; i >= 0; i--) {
682+
const message = decryptedMessage.additionalMessages[i]
683+
if (message.messageId !== undefined && message.messageId.endsWith(CodeWhispererConstants.findingsSuffix)) {
684+
if (message.body !== undefined) {
685+
try {
686+
const aggregatedCodeScanIssues: AggregatedCodeScanIssue[] = JSON.parse(message.body)
687+
for (const aggregatedCodeScanIssue of aggregatedCodeScanIssues) {
688+
for (const issue of aggregatedCodeScanIssue.issues) {
689+
issue.visible = !CodeWhispererSettings.instance
690+
.getIgnoredSecurityIssues()
691+
.includes(issue.title)
692+
}
693+
}
694+
initSecurityScanRender(aggregatedCodeScanIssues, undefined, CodeAnalysisScope.PROJECT)
695+
SecurityIssueTreeViewProvider.focus()
696+
} catch (e) {
697+
languageClient.info('Failed to parse findings')
698+
}
699+
}
700+
decryptedMessage.additionalMessages.splice(i, 1)
701+
}
702+
}
703+
}
704+
652705
async function resolveChatResponse(
653706
requestMethod: string,
654707
params: any,

packages/amazonq/src/lsp/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export async function startLanguageServer(
169169
reroute: true,
170170
modelSelection: true,
171171
workspaceFilePath: vscode.workspace.workspaceFile?.fsPath,
172+
qCodeReviewInChat: true,
172173
},
173174
window: {
174175
notifications: true,

0 commit comments

Comments
 (0)