diff --git a/package-lock.json b/package-lock.json index bf904769992..deb0cee9bca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17386,11 +17386,11 @@ "license": "MIT" }, "node_modules/@types/eslint": { - "version": "8.44.8", + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -31607,6 +31607,7 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { + "@types/eslint": "^8.56.0", "mocha": "^10.1.0" }, "engines": { diff --git a/packages/amazonq/src/app/inline/EditRendering/displayImage.ts b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts index f25284c6b5a..a8b7b9c0d07 100644 --- a/packages/amazonq/src/app/inline/EditRendering/displayImage.ts +++ b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts @@ -24,7 +24,7 @@ export class EditDecorationManager { private currentImageDecoration: vscode.DecorationOptions | undefined private currentRemovedCodeDecorations: vscode.DecorationOptions[] = [] private acceptHandler: (() => void) | undefined - private rejectHandler: (() => void) | undefined + private rejectHandler: ((isDiscard: boolean) => void) | undefined constructor() { this.registerCommandHandlers() @@ -131,7 +131,7 @@ export class EditDecorationManager { svgImage: vscode.Uri, startLine: number, onAccept: () => Promise, - onReject: () => Promise, + onReject: (isDiscard: boolean) => Promise, originalCode: string, newCode: string, originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }> @@ -185,9 +185,9 @@ export class EditDecorationManager { }) // Register Esc key handler for rejecting suggestion - vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', () => { + vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', (isDiscard: boolean = false) => { if (this.rejectHandler) { - this.rejectHandler() + this.rejectHandler(isDiscard) } }) } @@ -416,20 +416,31 @@ export async function displaySvgDecoration( // ) // } }, - async () => { + async (isDiscard: boolean) => { // Handle reject - getLogger().info('Edit suggestion rejected') + if (isDiscard) { + getLogger().info('Edit suggestion discarded') + } else { + getLogger().info('Edit suggestion rejected') + } await decorationManager.clearDecorations(editor) documentChangeListener.dispose() cursorChangeListener.dispose() + const suggestionState = isDiscard + ? { + seen: false, + accepted: false, + discarded: true, + } + : { + seen: true, + accepted: false, + discarded: false, + } const params: LogInlineCompletionSessionResultsParams = { sessionId: session.sessionId, completionSessionResult: { - [item.itemId]: { - seen: true, - accepted: false, - discarded: false, - }, + [item.itemId]: suggestionState, }, totalSessionDisplayTime: Date.now() - session.requestStartTime, firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, diff --git a/packages/amazonq/src/app/inline/editSuggestionState.ts b/packages/amazonq/src/app/inline/editSuggestionState.ts index 66a9211bdcf..27bcedd9345 100644 --- a/packages/amazonq/src/app/inline/editSuggestionState.ts +++ b/packages/amazonq/src/app/inline/editSuggestionState.ts @@ -8,12 +8,20 @@ */ export class EditSuggestionState { private static isEditSuggestionCurrentlyActive = false + private static displayStartTime = performance.now() static setEditSuggestionActive(active: boolean): void { this.isEditSuggestionCurrentlyActive = active + if (active) { + this.displayStartTime = performance.now() + } } static isEditSuggestionActive(): boolean { return this.isEditSuggestionCurrentlyActive } + + static isEditSuggestionDisplayingOverOneSecond(): boolean { + return this.isEditSuggestionActive() && performance.now() - this.displayStartTime > 1000 + } } diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 5f93fdff187..80833a4ae8f 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -8,8 +8,9 @@ import { inlineCompletionWithReferencesRequestType, TextDocumentContentChangeEvent, editCompletionRequestType, + LogInlineCompletionSessionResultsParams, } from '@aws/language-server-runtimes/protocol' -import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode' +import { CancellationToken, InlineCompletionContext, Position, TextDocument, commands } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' import { @@ -24,8 +25,8 @@ import { getLogger } from 'aws-core-vscode/shared' import { DocumentEventListener } from './documentEventListener' import { getOpenFilesInWindow } from 'aws-core-vscode/utils' import { asyncCallWithTimeout } from '../../util/timeoutUtil' -import { EditSuggestionState } from './editSuggestionState' import { extractFileContextInNotebooks } from './notebookUtil' +import { EditSuggestionState } from './editSuggestionState' export interface GetAllRecommendationsOptions { emitTelemetry?: boolean @@ -137,7 +138,7 @@ export class RecommendationService { * Completions use PartialResultToken with single 1 call of [getAllRecommendations]. * Edits leverage partialResultToken to achieve EditStreak such that clients can pull all continuous suggestions generated by the model within 1 EOS block. */ - if (!isTriggerByDeletion && !request.partialResultToken && !EditSuggestionState.isEditSuggestionActive()) { + if (!isTriggerByDeletion && !request.partialResultToken) { const completionPromise: Promise = languageClient.sendRequest( inlineCompletionWithReferencesRequestType.method, request, @@ -187,6 +188,39 @@ export class RecommendationService { })), }) + if (result.items.length > 0 && result.items[0].isInlineEdit === false) { + // Completion will not be rendered if an edit suggestion has been active for longer than 1 second + if (EditSuggestionState.isEditSuggestionDisplayingOverOneSecond()) { + const session = this.sessionManager.getActiveSession() + if (!session) { + return [] + } + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: Object.fromEntries( + result.items.map((item) => [ + item.itemId, + { + seen: false, + accepted: false, + discarded: true, + }, + ]) + ), + } + languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + this.sessionManager.clear() + getLogger().info( + 'Completion discarded due to active edit suggestion displayed longer than 1 second' + ) + return [] + } else if (EditSuggestionState.isEditSuggestionActive()) { + // discard the current edit suggestion if its display time is less than 1 sec + await commands.executeCommand('aws.amazonq.inline.rejectEdit', true) + getLogger().info('Discarding active edit suggestion displaying less than 1 second') + } + } + TelemetryHelper.instance.setSdkApiCallEndTime() TelemetryHelper.instance.setSessionId(result.sessionId) if (result.items.length > 0 && result.items[0].itemId !== undefined) { diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts index 7f2bcbb40ea..0b983f333b4 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -333,9 +333,9 @@ describe('RecommendationService', () => { } }) - it('should not make completion request when edit suggestion is active', async () => { + it('should make completion request when edit suggestion is active', async () => { // Mock EditSuggestionState to return true (edit suggestion is active) - const isEditSuggestionActiveStub = sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(true) + sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(true) const mockResult = { sessionId: 'test-session', @@ -360,20 +360,13 @@ describe('RecommendationService', () => { const completionCalls = cs.filter((c) => c.firstArg === completionApi) const editCalls = cs.filter((c) => c.firstArg === editApi) - assert.strictEqual(cs.length, 1) // Only edit call - assert.strictEqual(completionCalls.length, 0) // No completion calls + assert.strictEqual(cs.length, 2) // Only edit call + assert.strictEqual(completionCalls.length, 1) // No completion calls assert.strictEqual(editCalls.length, 1) // One edit call - - // Verify the stub was called - sinon.assert.calledOnce(isEditSuggestionActiveStub) }) it('should make completion request when edit suggestion is not active', async () => { // Mock EditSuggestionState to return false (no edit suggestion active) - const isEditSuggestionActiveStub = sandbox - .stub(EditSuggestionState, 'isEditSuggestionActive') - .returns(false) - const mockResult = { sessionId: 'test-session', items: [mockInlineCompletionItemOne], @@ -400,9 +393,6 @@ describe('RecommendationService', () => { assert.strictEqual(cs.length, 2) // Both calls assert.strictEqual(completionCalls.length, 1) // One completion call assert.strictEqual(editCalls.length, 1) // One edit call - - // Verify the stub was called - sinon.assert.calledOnce(isEditSuggestionActiveStub) }) }) }) diff --git a/plugins/eslint-plugin-aws-toolkits/package.json b/plugins/eslint-plugin-aws-toolkits/package.json index b10e57b1c38..924b08e2b95 100644 --- a/plugins/eslint-plugin-aws-toolkits/package.json +++ b/plugins/eslint-plugin-aws-toolkits/package.json @@ -9,6 +9,7 @@ "clean": "ts-node ../../scripts/clean.ts dist" }, "devDependencies": { + "@types/eslint": "^8.56.0", "mocha": "^10.1.0" }, "engines": {