Skip to content

Commit 14611c3

Browse files
authored
fix(amazonq): Don't show inline completions when a edit is displayed (#7839)
## Problem Inline completion suggestions were being shown even when edit suggestions were active, creating a confusing user experience where both types of suggestions could appear simultaneously. ## Solution 1. Added conflict prevention logic in provideInlineCompletionItems() to check for active edit suggestions using the existing aws.amazonq.editSuggestionActive context flag 1. Created isEditSuggestionActive() method to encapsulate context checking 1. Implemented DISCARD telemetry for completion suggestions that can't be shown due to active edits 1. Added unit tests covering the new functionality, including edge cases for mixed suggestion types and items without IDs 1. Updated displayImage.ts to properly set/unset the context flag when edit suggestions are displayed/cleared 1. The solution ensures only one type of suggestion is active at a time while maintaining telemetry compliance and following existing codebase patterns. --- 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.
1 parent 12efddd commit 14611c3

File tree

8 files changed

+392
-16
lines changed

8 files changed

+392
-16
lines changed

packages/amazonq/src/app/inline/EditRendering/displayImage.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { LogInlineCompletionSessionResultsParams } from '@aws/language-server-ru
1212
import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol'
1313
import path from 'path'
1414
import { imageVerticalOffset } from './svgGenerator'
15-
import { AmazonQInlineCompletionItemProvider } from '../completion'
15+
import { EditSuggestionState } from '../editSuggestionState'
16+
import type { AmazonQInlineCompletionItemProvider } from '../completion'
1617
import { vsCodeState } from 'aws-core-vscode/codewhisperer'
1718

1819
export class EditDecorationManager {
@@ -136,6 +137,7 @@ export class EditDecorationManager {
136137
await this.clearDecorations(editor)
137138

138139
await setContext('aws.amazonq.editSuggestionActive' as any, true)
140+
EditSuggestionState.setEditSuggestionActive(true)
139141

140142
this.acceptHandler = onAccept
141143
this.rejectHandler = onReject
@@ -166,6 +168,7 @@ export class EditDecorationManager {
166168
this.acceptHandler = undefined
167169
this.rejectHandler = undefined
168170
await setContext('aws.amazonq.editSuggestionActive' as any, false)
171+
EditSuggestionState.setEditSuggestionActive(false)
169172
}
170173

171174
/**
@@ -270,6 +273,28 @@ function getEndOfEditPosition(originalCode: string, newCode: string): vscode.Pos
270273
return editor ? editor.selection.active : new vscode.Position(0, 0)
271274
}
272275

276+
/**
277+
* Helper function to create discard telemetry params
278+
*/
279+
function createDiscardTelemetryParams(
280+
session: CodeWhispererSession,
281+
item: InlineCompletionItemWithReferences
282+
): LogInlineCompletionSessionResultsParams {
283+
return {
284+
sessionId: session.sessionId,
285+
completionSessionResult: {
286+
[item.itemId]: {
287+
seen: false,
288+
accepted: false,
289+
discarded: true,
290+
},
291+
},
292+
totalSessionDisplayTime: Date.now() - session.requestStartTime,
293+
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
294+
isInlineEdit: true,
295+
}
296+
}
297+
273298
/**
274299
* Helper function to display SVG decorations
275300
*/
@@ -286,21 +311,18 @@ export async function displaySvgDecoration(
286311
) {
287312
const originalCode = editor.document.getText()
288313

314+
// Check if a completion suggestion is currently active - if so, discard edit suggestion
315+
if (inlineCompletionProvider && (await inlineCompletionProvider.isCompletionActive())) {
316+
// Emit DISCARD telemetry for edit suggestion that can't be shown due to active completion
317+
const params = createDiscardTelemetryParams(session, item)
318+
languageClient.sendNotification('aws/logInlineCompletionSessionResults', params)
319+
getLogger().info('Edit suggestion discarded due to active completion suggestion')
320+
return
321+
}
322+
289323
const isPatchValid = applyPatch(editor.document.getText(), item.insertText as string)
290324
if (!isPatchValid) {
291-
const params: LogInlineCompletionSessionResultsParams = {
292-
sessionId: session.sessionId,
293-
completionSessionResult: {
294-
[item.itemId]: {
295-
seen: false,
296-
accepted: false,
297-
discarded: true,
298-
},
299-
},
300-
totalSessionDisplayTime: Date.now() - session.requestStartTime,
301-
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
302-
isInlineEdit: true,
303-
}
325+
const params = createDiscardTelemetryParams(session, item)
304326
// TODO: this session is closed on flare side hence discarded is not emitted in flare
305327
languageClient.sendNotification('aws/logInlineCompletionSessionResults', params)
306328
return

packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getLogger } from 'aws-core-vscode/shared'
1010
import { LanguageClient } from 'vscode-languageclient'
1111
import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol'
1212
import { CodeWhispererSession } from '../sessionManager'
13-
import { AmazonQInlineCompletionItemProvider } from '../completion'
13+
import type { AmazonQInlineCompletionItemProvider } from '../completion'
1414

1515
export async function showEdits(
1616
item: InlineCompletionItemWithReferences,

packages/amazonq/src/app/inline/completion.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,53 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
237237
await vscode.commands.executeCommand(`aws.amazonq.checkInlineSuggestionVisibility`)
238238
}
239239

240+
/**
241+
* Check if a completion suggestion is currently active/displayed
242+
*/
243+
public async isCompletionActive(): Promise<boolean> {
244+
const session = this.sessionManager.getActiveSession()
245+
if (session === undefined || !session.displayed || session.suggestions.some((item) => item.isInlineEdit)) {
246+
return false
247+
}
248+
249+
// Use VS Code command to check if inline suggestion is actually visible on screen
250+
// This command only executes when inlineSuggestionVisible context is true
251+
await vscode.commands.executeCommand('aws.amazonq.checkInlineSuggestionVisibility')
252+
const isInlineSuggestionVisible = performance.now() - session.lastVisibleTime < 50
253+
return isInlineSuggestionVisible
254+
}
255+
256+
/**
257+
* Batch discard telemetry for completion suggestions when edit suggestion is active
258+
*/
259+
public batchDiscardTelemetryForEditSuggestion(items: any[], session: any): void {
260+
// Emit DISCARD telemetry for completion suggestions that can't be shown due to active edit
261+
const completionSessionResult: {
262+
[key: string]: { seen: boolean; accepted: boolean; discarded: boolean }
263+
} = {}
264+
265+
for (const item of items) {
266+
if (!item.isInlineEdit && item.itemId) {
267+
completionSessionResult[item.itemId] = {
268+
seen: false,
269+
accepted: false,
270+
discarded: true,
271+
}
272+
}
273+
}
274+
275+
// Send single telemetry event for all discarded items
276+
if (Object.keys(completionSessionResult).length > 0) {
277+
const params: LogInlineCompletionSessionResultsParams = {
278+
sessionId: session.sessionId,
279+
completionSessionResult,
280+
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
281+
totalSessionDisplayTime: performance.now() - session.requestStartTime,
282+
}
283+
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
284+
}
285+
}
286+
240287
// this method is automatically invoked by VS Code as user types
241288
async provideInlineCompletionItems(
242289
document: TextDocument,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* Manages the state of edit suggestions to avoid circular dependencies
8+
*/
9+
export class EditSuggestionState {
10+
private static isEditSuggestionCurrentlyActive = false
11+
12+
static setEditSuggestionActive(active: boolean): void {
13+
this.isEditSuggestionCurrentlyActive = active
14+
}
15+
16+
static isEditSuggestionActive(): boolean {
17+
return this.isEditSuggestionCurrentlyActive
18+
}
19+
}

packages/amazonq/src/app/inline/recommendationService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { getLogger } from 'aws-core-vscode/shared'
2424
import { DocumentEventListener } from './documentEventListener'
2525
import { getOpenFilesInWindow } from 'aws-core-vscode/utils'
2626
import { asyncCallWithTimeout } from '../../util/timeoutUtil'
27+
import { EditSuggestionState } from './editSuggestionState'
2728

2829
export interface GetAllRecommendationsOptions {
2930
emitTelemetry?: boolean
@@ -132,7 +133,7 @@ export class RecommendationService {
132133
* Completions use PartialResultToken with single 1 call of [getAllRecommendations].
133134
* Edits leverage partialResultToken to achieve EditStreak such that clients can pull all continuous suggestions generated by the model within 1 EOS block.
134135
*/
135-
if (!isTriggerByDeletion && !request.partialResultToken) {
136+
if (!isTriggerByDeletion && !request.partialResultToken && !EditSuggestionState.isEditSuggestionActive()) {
136137
const completionPromise: Promise<InlineCompletionListWithReferences> = languageClient.sendRequest(
137138
inlineCompletionWithReferencesRequestType.method,
138139
request,

packages/amazonq/src/app/inline/sessionManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface CodeWhispererSession {
2626
triggerOnAcceptance?: boolean
2727
// whether any suggestion in this session was displayed on screen
2828
displayed: boolean
29+
// timestamp when the suggestion was last visible
30+
lastVisibleTime: number
2931
}
3032

3133
export class SessionManager {
@@ -52,6 +54,7 @@ export class SessionManager {
5254
firstCompletionDisplayLatency,
5355
diagnosticsBeforeAccept,
5456
displayed: false,
57+
lastVisibleTime: 0,
5558
}
5659
this._currentSuggestionIndex = 0
5760
}
@@ -134,6 +137,7 @@ export class SessionManager {
134137
public checkInlineSuggestionVisibility() {
135138
if (this.activeSession) {
136139
this.activeSession.displayed = true
140+
this.activeSession.lastVisibleTime = performance.now()
137141
}
138142
}
139143

packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CursorUpdateManager } from '../../../../../src/app/inline/cursorUpdateM
1515
import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer'
1616
import { globals } from 'aws-core-vscode/shared'
1717
import { DocumentEventListener } from '../../../../../src/app/inline/documentEventListener'
18+
import { EditSuggestionState } from '../../../../../src/app/inline/editSuggestionState'
1819

1920
const completionApi = 'aws/textDocument/inlineCompletionWithReferences'
2021
const editApi = 'aws/textDocument/editCompletion'
@@ -325,5 +326,77 @@ describe('RecommendationService', () => {
325326
console.error = originalConsoleError
326327
}
327328
})
329+
330+
it('should not make completion request when edit suggestion is active', async () => {
331+
// Mock EditSuggestionState to return true (edit suggestion is active)
332+
const isEditSuggestionActiveStub = sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(true)
333+
334+
const mockResult = {
335+
sessionId: 'test-session',
336+
items: [mockInlineCompletionItemOne],
337+
partialResultToken: undefined,
338+
}
339+
340+
sendRequestStub.resolves(mockResult)
341+
342+
await service.getAllRecommendations(
343+
languageClient,
344+
mockDocument,
345+
mockPosition,
346+
mockContext,
347+
mockToken,
348+
true,
349+
mockDocumentEventListener
350+
)
351+
352+
// Verify sendRequest was called only for edit API, not completion API
353+
const cs = sendRequestStub.getCalls()
354+
const completionCalls = cs.filter((c) => c.firstArg === completionApi)
355+
const editCalls = cs.filter((c) => c.firstArg === editApi)
356+
357+
assert.strictEqual(cs.length, 1) // Only edit call
358+
assert.strictEqual(completionCalls.length, 0) // No completion calls
359+
assert.strictEqual(editCalls.length, 1) // One edit call
360+
361+
// Verify the stub was called
362+
sinon.assert.calledOnce(isEditSuggestionActiveStub)
363+
})
364+
365+
it('should make completion request when edit suggestion is not active', async () => {
366+
// Mock EditSuggestionState to return false (no edit suggestion active)
367+
const isEditSuggestionActiveStub = sandbox
368+
.stub(EditSuggestionState, 'isEditSuggestionActive')
369+
.returns(false)
370+
371+
const mockResult = {
372+
sessionId: 'test-session',
373+
items: [mockInlineCompletionItemOne],
374+
partialResultToken: undefined,
375+
}
376+
377+
sendRequestStub.resolves(mockResult)
378+
379+
await service.getAllRecommendations(
380+
languageClient,
381+
mockDocument,
382+
mockPosition,
383+
mockContext,
384+
mockToken,
385+
true,
386+
mockDocumentEventListener
387+
)
388+
389+
// Verify sendRequest was called for both APIs
390+
const cs = sendRequestStub.getCalls()
391+
const completionCalls = cs.filter((c) => c.firstArg === completionApi)
392+
const editCalls = cs.filter((c) => c.firstArg === editApi)
393+
394+
assert.strictEqual(cs.length, 2) // Both calls
395+
assert.strictEqual(completionCalls.length, 1) // One completion call
396+
assert.strictEqual(editCalls.length, 1) // One edit call
397+
398+
// Verify the stub was called
399+
sinon.assert.calledOnce(isEditSuggestionActiveStub)
400+
})
328401
})
329402
})

0 commit comments

Comments
 (0)