Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 21 additions & 0 deletions packages/amazonq/src/app/inline/EditRendering/displayImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,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 && 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
42 changes: 41 additions & 1 deletion packages/amazonq/src/app/inline/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
import { LineTracker } from './stateTracker/lineTracker'
import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation'
import { TelemetryHelper } from './telemetryHelper'
import { Experiments, getLogger, sleep } from 'aws-core-vscode/shared'
import { Experiments, getContext, getLogger, sleep } from 'aws-core-vscode/shared'
import { messageUtils } from 'aws-core-vscode/utils'
import { showEdits } from './EditRendering/imageRenderer'
import { ICursorUpdateRecorder } from './cursorUpdateManager'
Expand Down Expand Up @@ -237,6 +237,21 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
await vscode.commands.executeCommand(`aws.amazonq.checkInlineSuggestionVisibility`)
}

/**
* Check if a completion suggestion is currently active/displayed
*/
public isCompletionActive(): boolean {
const session = this.sessionManager.getActiveSession()
return session !== undefined && session.displayed && !session.suggestions.some((item) => item.isInlineEdit)
Copy link
Contributor

Choose a reason for hiding this comment

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

session.displayed is a flag that indicates if a session has ever been displayed or not. There is an edge case when it is displayed but user did not reject it, instead moved cursor around or kept typing, in that case, the session.display is true but nothing is rendered on screen.

Copy link
Contributor

Choose a reason for hiding this comment

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

await vscode.commands.executeCommand(aws.amazonq.checkInlineSuggestionVisibility) is the real method to check if anything is rendered on screen or not because this command is defined under a context variable of VS Code and it only runs if inline suggest is visible

Copy link
Contributor Author

@floralph floralph Aug 8, 2025

Choose a reason for hiding this comment

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

Fixed as suggested.

}

/**
* Check if an edit suggestion is currently active
*/
private isEditSuggestionActive(): boolean {
return getContext('aws.amazonq.editSuggestionActive') || false
Copy link
Contributor

Choose a reason for hiding this comment

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

getContext does not necessarily work due to vs code limitations

/**

  • Returns the value of a context key set via {@link setContext} wrapper for this session.
  • Warning: this does not guarantee the state of the context key in vscode because it may have
  • been set via vscode.commands.executeCommand('setContext'). It has no connection the
  • context keys stored in vscode itself because an API for this is not exposed.
    */
    export function getContext(key: contextKey): any {
    return contextMap[key]
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed - The command approach I tried had reliability issues with when clause evaluation. I ended up using a simple boolean property synchronized with setContext calls instead.

}

// this method is automatically invoked by VS Code as user types
async provideInlineCompletionItems(
document: TextDocument,
Expand Down Expand Up @@ -435,6 +450,31 @@ ${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()) {
// Emit DISCARD telemetry for completion suggestions that can't be shown due to active edit
for (const item of items) {
if (!item.isInlineEdit && item.itemId) {
const params: LogInlineCompletionSessionResultsParams = {
sessionId: session.sessionId,
completionSessionResult: {
[item.itemId]: {
seen: false,
accepted: false,
discarded: true,
},
},
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
totalSessionDisplayTime: performance.now() - session.requestStartTime,
}
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
}
}
Copy link
Contributor

@andrewyuq andrewyuq Aug 7, 2025

Choose a reason for hiding this comment

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

For UTD we only want to send 1 event for trigger (especially for completion suggestions)
all the items in this completion are discarded we can just send a list of items in completionSessionResult and only one call of this.languageClient.sendNotification(this.logSessionResultMessageName, params)

Copy link
Contributor

Choose a reason for hiding this comment

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

yep we only need 1 call of logSessionResult per trigger

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated code to batch completionSessionResult and only send telemetry once.

this.sessionManager.clear()
logstr += `- completion suggestions discarded due to active edit suggestion`
return []
}

const itemsMatchingTypeahead = []

for (const item of items) {
Expand Down
119 changes: 119 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 @@ -419,6 +419,125 @@ 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
)

// Stub the private method to return true (following existing pattern)
sandbox.stub(provider as any, 'isEditSuggestionActive').returns(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
)

sandbox.stub(provider as any, 'isEditSuggestionActive').returns(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
)

sandbox.stub(provider as any, 'isEditSuggestionActive').returns(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
)

// Since getContext returns undefined by default, this should return false
const result = (provider as any).isEditSuggestionActive()
assert.strictEqual(result, false)
})
})
})
})
})
Loading