Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/amazonq/src/app/inline/EditRendering/displayImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { LogInlineCompletionSessionResultsParams } from '@aws/language-server-ru
import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol'
import path from 'path'
import { imageVerticalOffset } from './svgGenerator'
import { AmazonQInlineCompletionItemProvider } from '../completion'
import { EditSuggestionState } from '../editSuggestionState'
import type { AmazonQInlineCompletionItemProvider } from '../completion'
import { vsCodeState } from 'aws-core-vscode/codewhisperer'

export class EditDecorationManager {
Expand Down Expand Up @@ -136,6 +137,7 @@ export class EditDecorationManager {
await this.clearDecorations(editor)

await setContext('aws.amazonq.editSuggestionActive' as any, true)
EditSuggestionState.setEditSuggestionActive(true)

this.acceptHandler = onAccept
this.rejectHandler = onReject
Expand Down Expand Up @@ -166,6 +168,7 @@ export class EditDecorationManager {
this.acceptHandler = undefined
this.rejectHandler = undefined
await setContext('aws.amazonq.editSuggestionActive' as any, false)
EditSuggestionState.setEditSuggestionActive(false)
}

/**
Expand Down Expand Up @@ -286,6 +289,27 @@ export async function displaySvgDecoration(
) {
const originalCode = editor.document.getText()

// Check if a completion suggestion is currently active - if so, discard edit suggestion
if (inlineCompletionProvider && (await inlineCompletionProvider.isCompletionActive())) {
// Emit DISCARD telemetry for edit suggestion that can't be shown due to active completion
const params: LogInlineCompletionSessionResultsParams = {
sessionId: session.sessionId,
completionSessionResult: {
[item.itemId]: {
seen: false,
accepted: false,
discarded: true,
},
},
totalSessionDisplayTime: Date.now() - session.requestStartTime,
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
isInlineEdit: true,
}
languageClient.sendNotification('aws/logInlineCompletionSessionResults', params)
getLogger().info('Edit suggestion discarded due to active completion suggestion')
return
}

await decorationManager.displayEditSuggestion(
editor,
svgImage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getLogger } from 'aws-core-vscode/shared'
import { LanguageClient } from 'vscode-languageclient'
import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol'
import { CodeWhispererSession } from '../sessionManager'
import { AmazonQInlineCompletionItemProvider } from '../completion'
import type { AmazonQInlineCompletionItemProvider } from '../completion'

export async function showEdits(
item: InlineCompletionItemWithReferences,
Expand Down
66 changes: 66 additions & 0 deletions packages/amazonq/src/app/inline/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { TelemetryHelper } from './telemetryHelper'
import { Experiments, getLogger, sleep } from 'aws-core-vscode/shared'
import { messageUtils } from 'aws-core-vscode/utils'
import { showEdits } from './EditRendering/imageRenderer'
import { EditSuggestionState } from './editSuggestionState'
import { ICursorUpdateRecorder } from './cursorUpdateManager'
import { DocumentEventListener } from './documentEventListener'

Expand Down Expand Up @@ -237,6 +238,63 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
await vscode.commands.executeCommand(`aws.amazonq.checkInlineSuggestionVisibility`)
}

/**
* Check if a completion suggestion is currently active/displayed
*/
public async isCompletionActive(): Promise<boolean> {
const session = this.sessionManager.getActiveSession()
if (session === undefined || !session.displayed || session.suggestions.some((item) => item.isInlineEdit)) {
return false
}

// Use VS Code command to check if inline suggestion is actually visible on screen
// This command only executes when inlineSuggestionVisible context is true
try {
await vscode.commands.executeCommand('aws.amazonq.checkInlineSuggestionVisibility')
return true
} catch {
return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you managed to get into this catch block?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented your timestamp-based solution using performance.now() in the checkInlineSuggestionVisibility()
method and checking if it was called within the last 50ms.

}
}

/**
* Batch discard telemetry for completion suggestions when edit suggestion is active
*/
public batchDiscardTelemetryForEditSuggestion(items: any[], session: any): void {
// Emit DISCARD telemetry for completion suggestions that can't be shown due to active edit
const completionSessionResult: {
[key: string]: { seen: boolean; accepted: boolean; discarded: boolean }
} = {}

for (const item of items) {
if (!item.isInlineEdit && item.itemId) {
completionSessionResult[item.itemId] = {
seen: false,
accepted: false,
discarded: true,
}
}
}

// Send single telemetry event for all discarded items
if (Object.keys(completionSessionResult).length > 0) {
const params: LogInlineCompletionSessionResultsParams = {
sessionId: session.sessionId,
completionSessionResult,
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
totalSessionDisplayTime: performance.now() - session.requestStartTime,
}
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
}
}

/**
* Check if an edit suggestion is currently active
*/
private isEditSuggestionActive(): boolean {
return EditSuggestionState.isEditSuggestionActive()
}

// this method is automatically invoked by VS Code as user types
async provideInlineCompletionItems(
document: TextDocument,
Expand Down Expand Up @@ -435,6 +493,14 @@ ${itemLog}
// the user typed characters from invoking suggestion cursor position to receiving suggestion position
const typeahead = document.getText(new Range(position, editor.selection.active))

// Check if an edit suggestion is currently active - if so, discard completion suggestions
if (this.isEditSuggestionActive()) {
this.batchDiscardTelemetryForEditSuggestion(items, session)
this.sessionManager.clear()
logstr += `- completion suggestions discarded due to active edit suggestion`
return []
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A newer edit suggestion arriving from the server. In such cases, the currently displayed edit suggestion is hidden from the user with a DISCARD state, and replaced with the newer edit suggestion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1,

if (isEditSuggestionActive && !isInlineEdit) {
    return
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other way i think you can also do

if (!isTriggerByDeletion && !request.partialResultToken && !isEditSuggestionActive) {
    promise.push(completionPromise)
}

here in recommendationService https://github.com/aws/aws-toolkit-vscode/blob/master/packages/amazonq/src/app/inline/recommendationService.ts#L135-L141

Copy link
Contributor

@Will-ShaoHua Will-ShaoHua Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think since the only scenario where 2 overlapping suggestion will happen is , correct me if i'm wrong ralph

  1. when there is an Edit being shown
  2. the Edit is not dismissed by users
  3. users again start typing and trigger "Completion"

by doing this, "Completion" is not even fired and you dont even need to consider those suggestion states

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented Will's second solution - added !EditSuggestionState.isEditSuggestionActive() to the condition in recommendationService.ts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can remove this

  // Check if an edit suggestion is currently active - if so, discard completion suggestions
            if (this.isEditSuggestionActive()) {
                this.batchDiscardTelemetryForEditSuggestion(items, session)
                this.sessionManager.clear()
                logstr += `- completion suggestions discarded due to active edit suggestion`
                return []
            }           

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ow it will preventing

A newer edit suggestion arriving from the server. In such cases, the currently displayed edit suggestion is hidden from the user with a DISCARD state, and replaced with the newer edit suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

const itemsMatchingTypeahead = []

for (const item of items) {
Expand Down
19 changes: 19 additions & 0 deletions packages/amazonq/src/app/inline/editSuggestionState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Manages the state of edit suggestions to avoid circular dependencies
*/
export class EditSuggestionState {
private static isEditSuggestionCurrentlyActive = false

static setEditSuggestionActive(active: boolean): void {
this.isEditSuggestionCurrentlyActive = active
}

static isEditSuggestionActive(): boolean {
return this.isEditSuggestionCurrentlyActive
}
}
138 changes: 138 additions & 0 deletions packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import assert from 'assert'
import { LanguageClient } from 'vscode-languageclient'
import { StringValue } from 'vscode-languageserver-types'
import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion'
import { EditSuggestionState } from '../../../../../src/app/inline/editSuggestionState'
import { RecommendationService } from '../../../../../src/app/inline/recommendationService'
import { SessionManager } from '../../../../../src/app/inline/sessionManager'
import { createMockDocument, createMockTextEditor, getTestWindow, installFakeClock } from 'aws-core-vscode/test'
Expand Down Expand Up @@ -419,6 +420,143 @@ describe('InlineCompletionManager', () => {
assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(1, 26))
})
})

it('should return empty array and emit telemetry when edit suggestion is active', async () => {
provider = new AmazonQInlineCompletionItemProvider(
languageClient,
recommendationService,
mockSessionManager,
inlineTutorialAnnotation,
documentEventListener
)

// Set edit suggestion active to trigger discard behavior
EditSuggestionState.setEditSuggestionActive(true)

const result = await provider.provideInlineCompletionItems(
mockDocument,
mockPosition,
mockContext,
mockToken
)

// Should return empty array
assert.deepStrictEqual(result, [])

// Should emit telemetry for each completion suggestion
assert.strictEqual(sendNotificationStub.callCount, 2) // For both mockSuggestions

// Verify telemetry parameters for first call
const firstCall = sendNotificationStub.getCall(0)
assert.strictEqual(firstCall.args[0], 'aws/logInlineCompletionSessionResults')
const sessionResult = Object.values(firstCall.args[1].completionSessionResult)[0] as any
assert.strictEqual(sessionResult.seen, false)
assert.strictEqual(sessionResult.accepted, false)
assert.strictEqual(sessionResult.discarded, true)
})

it('should only emit telemetry for non-inline-edit items when edit is active', async () => {
provider = new AmazonQInlineCompletionItemProvider(
languageClient,
recommendationService,
mockSessionManager,
inlineTutorialAnnotation,
documentEventListener
)

EditSuggestionState.setEditSuggestionActive(true)

// Mix of inline edits and completions
const mixedSuggestions = [
{ itemId: 'edit1', insertText: 'diff', isInlineEdit: true },
{ itemId: 'completion1', insertText: 'code', isInlineEdit: false },
]
getActiveRecommendationStub.returns(mixedSuggestions)

const result = await provider.provideInlineCompletionItems(
mockDocument,
mockPosition,
mockContext,
mockToken
)

// Should return empty array
assert.deepStrictEqual(result, [])

// Should only emit telemetry for completion, not inline edit
assert.strictEqual(sendNotificationStub.callCount, 1)
const call = sendNotificationStub.getCall(0)
assert(call.args[1].completionSessionResult['completion1'])
assert(!call.args[1].completionSessionResult['edit1'])
})

it('should not emit telemetry for items without itemId when edit is active', async () => {
provider = new AmazonQInlineCompletionItemProvider(
languageClient,
recommendationService,
mockSessionManager,
inlineTutorialAnnotation,
documentEventListener
)

EditSuggestionState.setEditSuggestionActive(true)

// Set up suggestions where some don't have itemId
const suggestionsWithoutId = [
{ insertText: 'code', isInlineEdit: false }, // No itemId
{ itemId: 'completion1', insertText: 'code', isInlineEdit: false },
]
getActiveRecommendationStub.returns(suggestionsWithoutId)

const result = await provider.provideInlineCompletionItems(
mockDocument,
mockPosition,
mockContext,
mockToken
)

// Should return empty array
assert.deepStrictEqual(result, [])

// Should only emit telemetry for the item with itemId
assert.strictEqual(sendNotificationStub.callCount, 1)
const call = sendNotificationStub.getCall(0)
assert(call.args[1].completionSessionResult['completion1'])
})

describe('isEditSuggestionActive', () => {
it('should return false when no edit suggestion is active', () => {
provider = new AmazonQInlineCompletionItemProvider(
languageClient,
recommendationService,
mockSessionManager,
inlineTutorialAnnotation,
documentEventListener
)

// Set the static property to false
EditSuggestionState.setEditSuggestionActive(false)

const result = (provider as any).isEditSuggestionActive()
assert.strictEqual(result, false)
})

it('should return true when edit suggestion is active', () => {
provider = new AmazonQInlineCompletionItemProvider(
languageClient,
recommendationService,
mockSessionManager,
inlineTutorialAnnotation,
documentEventListener
)

// Set the static property to true
EditSuggestionState.setEditSuggestionActive(true)

const result = (provider as any).isEditSuggestionActive()
assert.strictEqual(result, true)
})
})
})
})
})
Loading
Loading