Skip to content

Commit 2a414b3

Browse files
committed
Track SessionContext instead of InvocationContext
1. Since the shift from displaying one trigger at a time to displaying multiple triggers at a time, use SessionContext as the disposable for the session states. 2. Introduce the concept of "display session" which can show multiple triggers at a time and sessionContext is to hold states for that session. 3. ongoingRequests will be a map of <jobId, InvocationContext> where InvocationContext is the state of each trigger. 4. Managed these states globally by dynamically adding Trigger states into ongoingRequests and whenever the user made a decision(accept or reject), conclude decisions for all the ongoing triggers and clear ongoingRequest, so that a new display session can start later.
1 parent fa3dcf2 commit 2a414b3

16 files changed

+526
-558
lines changed

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererCon
4646
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization
4747
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
4848
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
49+
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
4950
import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
5051
import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
5152
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
@@ -92,6 +93,7 @@ interface CodeWhispererClientAdaptor : Disposable {
9293
fun listAvailableCustomizations(): List<CodeWhispererCustomization>
9394

9495
fun sendUserTriggerDecisionTelemetry(
96+
sessionContext: SessionContext,
9597
requestContext: RequestContext,
9698
responseContext: ResponseContext,
9799
completionType: CodewhispererCompletionType,
@@ -293,6 +295,7 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
293295
}
294296

295297
override fun sendUserTriggerDecisionTelemetry(
298+
sessionContext: SessionContext,
296299
requestContext: RequestContext,
297300
responseContext: ResponseContext,
298301
completionType: CodewhispererCompletionType,
@@ -303,24 +306,24 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
303306
): SendTelemetryEventResponse {
304307
val fileContext = requestContext.fileContextInfo
305308
val programmingLanguage = fileContext.programmingLanguage
306-
var e2eLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency()
309+
var e2eLatency = sessionContext.latencyContext.getCodeWhispererEndToEndLatency()
307310

308311
// When we send a userTriggerDecision of Empty or Discard, we set the time users see the first
309312
// suggestion to be now.
310313
if (e2eLatency < 0) {
311314
e2eLatency = TimeUnit.NANOSECONDS.toMillis(
312-
System.nanoTime() - requestContext.latencyContext.codewhispererEndToEndStart
315+
System.nanoTime() - sessionContext.latencyContext.codewhispererEndToEndStart
313316
).toDouble()
314317
}
315318
return bearerClient().sendTelemetryEvent { requestBuilder ->
316319
requestBuilder.telemetryEvent { telemetryEventBuilder ->
317320
telemetryEventBuilder.userTriggerDecisionEvent {
318-
it.requestId(requestContext.latencyContext.firstRequestId)
321+
it.requestId(sessionContext.latencyContext.firstRequestId)
319322
it.completionType(completionType.toCodeWhispererSdkType())
320323
it.programmingLanguage { builder -> builder.languageName(programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) }
321324
it.sessionId(responseContext.sessionId)
322325
it.recommendationLatencyMilliseconds(e2eLatency)
323-
it.triggerToResponseLatencyMilliseconds(requestContext.latencyContext.paginationFirstCompletionTime)
326+
it.triggerToResponseLatencyMilliseconds(sessionContext.latencyContext.paginationFirstCompletionTime)
324327
it.suggestionState(suggestionState.toCodeWhispererSdkType())
325328
it.timestamp(Instant.now())
326329
it.suggestionReferenceCount(suggestionReferenceCount)

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorManager.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import com.intellij.openapi.editor.Editor
1111
import com.intellij.openapi.util.TextRange
1212
import com.intellij.psi.PsiDocumentManager
1313
import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition
14-
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
1514
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
1615
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
16+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
1717
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
1818
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CaretMovement
1919
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.PAIRED_BRACKETS
@@ -23,17 +23,21 @@ import java.util.Stack
2323

2424
@Service
2525
class CodeWhispererEditorManager {
26-
fun updateEditorWithRecommendation(states: InvocationContext, sessionContext: SessionContext) {
27-
val (requestContext, responseContext, recommendationContext) = states
28-
val (project, editor) = requestContext
26+
fun updateEditorWithRecommendation(sessionContext: SessionContext) {
27+
val previews = CodeWhispererService.getInstance().getAllSuggestionsPreviewInfo()
28+
val selectedIndex = sessionContext.selectedIndex
29+
val preview = previews[selectedIndex]
30+
val states = CodeWhispererService.getInstance().getAllPaginationSessions()[preview.jobId] ?: return
31+
val (requestContext, responseContext) = states
32+
val (project, editor) = sessionContext
2933
val document = editor.document
3034
val primaryCaret = editor.caretModel.primaryCaret
31-
val selectedIndex = sessionContext.selectedIndex
32-
val typeahead = sessionContext.typeahead
33-
val detail = recommendationContext.details[selectedIndex]
35+
val typeahead = preview.typeahead
36+
val detail = preview.detail
37+
val userInput = preview.userInput
3438
val reformatted = CodeWhispererPopupManager.getInstance().getReformattedRecommendation(
3539
detail,
36-
recommendationContext.userInputSinceInvocation
40+
userInput
3741
)
3842
val remainingRecommendation = reformatted.substring(typeahead.length)
3943
val originalOffset = primaryCaret.offset - typeahead.length
@@ -43,6 +47,8 @@ class CodeWhispererEditorManager {
4347
val insertEndOffset = sessionContext.insertEndOffset
4448
val endOffsetToReplace = if (insertEndOffset != -1) insertEndOffset else primaryCaret.offset
4549

50+
preview.detail.isAccepted = true
51+
4652
WriteCommandAction.runWriteCommandAction(project) {
4753
document.replaceString(originalOffset, endOffsetToReplace, reformatted)
4854
PsiDocumentManager.getInstance(project).commitDocument(document)
@@ -67,7 +73,7 @@ class CodeWhispererEditorManager {
6773

6874
ApplicationManager.getApplication().messageBus.syncPublisher(
6975
CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED,
70-
).afterAccept(states, sessionContext, rangeMarker)
76+
).afterAccept(states, previews, sessionContext, rangeMarker)
7177
}
7278
}
7379
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/editor/CodeWhispererEditorUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ object CodeWhispererEditorUtil {
9595
}
9696

9797
fun shouldSkipInvokingBasedOnRightContext(editor: Editor): Boolean {
98-
val caretContext = runReadAction { CodeWhispererEditorUtil.extractCaretContext(editor) }
98+
val caretContext = runReadAction { extractCaretContext(editor) }
9999
val rightContextLines = caretContext.rightFileContext.split(Regex("\r?\n"))
100100
val rightContextCurrentLine = if (rightContextLines.isEmpty()) "" else rightContextLines[0]
101101

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdder.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import software.aws.toolkits.core.utils.getLogger
1414
import software.aws.toolkits.core.utils.info
1515
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
1616
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
17+
import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
1718
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
1819

1920
abstract class CodeWhispererImportAdder {
2021
abstract val supportedLanguages: List<CodeWhispererProgrammingLanguage>
2122
abstract val dummyFileName: String
2223

23-
fun insertImportStatements(states: InvocationContext, sessionContext: SessionContext) {
24-
val imports = states.recommendationContext.details[sessionContext.selectedIndex]
25-
.recommendation.mostRelevantMissingImports()
24+
fun insertImportStatements(states: InvocationContext, previews: List<PreviewContext>, sessionContext: SessionContext) {
25+
val imports = previews[sessionContext.selectedIndex].detail.recommendation.mostRelevantMissingImports()
2626
LOG.info { "Adding ${imports.size} imports for completions, sessionId: ${states.responseContext.sessionId}" }
2727
imports.forEach {
2828
insertImportStatement(states, it)

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/importadder/CodeWhispererImportAdderListener.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import com.intellij.openapi.editor.RangeMarker
77
import software.aws.toolkits.core.utils.debug
88
import software.aws.toolkits.core.utils.getLogger
99
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
10+
import software.aws.toolkits.jetbrains.services.codewhisperer.model.PreviewContext
1011
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
1112
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererUserActionListener
1213
import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings
1314

1415
object CodeWhispererImportAdderListener : CodeWhispererUserActionListener {
1516
internal val LOG = getLogger<CodeWhispererImportAdderListener>()
16-
override fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {
17+
override fun afterAccept(states: InvocationContext, previews: List<PreviewContext>, sessionContext: SessionContext, rangeMarker: RangeMarker) {
1718
if (!CodeWhispererSettings.getInstance().isImportAdderEnabled()) {
1819
LOG.debug { "Import adder not enabled in user settings" }
1920
return
@@ -28,6 +29,6 @@ object CodeWhispererImportAdderListener : CodeWhispererUserActionListener {
2829
LOG.debug { "No import adder found for $language" }
2930
return
3031
}
31-
importAdder.insertImportStatements(states, sessionContext)
32+
importAdder.insertImportStatements(states, previews, sessionContext)
3233
}
3334
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/CodeWhispererInlayManager.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,26 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.inlay
66
import com.intellij.idea.AppMode
77
import com.intellij.openapi.components.Service
88
import com.intellij.openapi.components.service
9-
import com.intellij.openapi.editor.Editor
109
import com.intellij.openapi.editor.EditorCustomElementRenderer
1110
import com.intellij.openapi.editor.Inlay
12-
import com.intellij.openapi.ui.popup.JBPopup
1311
import com.intellij.openapi.util.Disposer
14-
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
1512
import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk
13+
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
1614

1715
@Service
1816
class CodeWhispererInlayManager {
1917
private val existingInlays = mutableListOf<Inlay<EditorCustomElementRenderer>>()
20-
fun updateInlays(states: InvocationContext, chunks: List<RecommendationChunk>) {
21-
val editor = states.requestContext.editor
18+
fun updateInlays(sessionContext: SessionContext, chunks: List<RecommendationChunk>) {
2219
clearInlays()
2320

2421
chunks.forEach { chunk ->
25-
createCodeWhispererInlays(editor, chunk.inlayOffset, chunk.text, states.popup)
22+
createCodeWhispererInlays(sessionContext, chunk.inlayOffset, chunk.text)
2623
}
2724
}
2825

29-
private fun createCodeWhispererInlays(editor: Editor, startOffset: Int, inlayText: String, popup: JBPopup) {
26+
private fun createCodeWhispererInlays(sessionContext: SessionContext, startOffset: Int, inlayText: String) {
3027
if (inlayText.isEmpty()) return
28+
val editor = sessionContext.editor
3129
val firstNewlineIndex = inlayText.indexOf("\n")
3230
val firstLine: String
3331
val otherLines: String
@@ -49,7 +47,7 @@ class CodeWhispererInlayManager {
4947
val inlineInlay = editor.inlayModel.addInlineElement(startOffset, true, firstLineRenderer)
5048
inlineInlay?.let {
5149
existingInlays.add(it)
52-
Disposer.register(popup, it)
50+
Disposer.register(sessionContext, it)
5351
}
5452
}
5553

@@ -73,7 +71,7 @@ class CodeWhispererInlayManager {
7371
)
7472
blockInlay?.let {
7573
existingInlays.add(it)
76-
Disposer.register(popup, it)
74+
Disposer.register(sessionContext, it)
7775
}
7876
}
7977
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/model/CodeWhispererModel.kt

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,37 @@
44
package software.aws.toolkits.jetbrains.services.codewhisperer.model
55

66
import com.intellij.openapi.Disposable
7+
import com.intellij.openapi.editor.Editor
78
import com.intellij.openapi.editor.VisualPosition
89
import com.intellij.openapi.editor.markup.RangeHighlighter
10+
import com.intellij.openapi.project.Project
911
import com.intellij.openapi.ui.popup.JBPopup
12+
import com.intellij.openapi.util.Disposer
1013
import com.intellij.openapi.vfs.VirtualFile
14+
import com.intellij.util.concurrency.annotations.RequiresEdt
1115
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
1216
import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse
1317
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
1418
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadContext
1519
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
20+
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
1621
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
22+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererIntelliSenseOnHoverListener
23+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
24+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
1725
import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
1826
import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
27+
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
1928
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
29+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.setIntelliSensePopupAlpha
2030
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CrossFileStrategy
2131
import software.aws.toolkits.jetbrains.services.codewhisperer.util.SupplementalContextStrategy
2232
import software.aws.toolkits.jetbrains.services.codewhisperer.util.UtgStrategy
2333
import software.aws.toolkits.telemetry.CodewhispererCompletionType
2434
import software.aws.toolkits.telemetry.CodewhispererTriggerType
2535
import software.aws.toolkits.telemetry.Result
36+
import java.time.Duration
37+
import java.time.Instant
2638
import java.util.concurrent.TimeUnit
2739

2840
data class Chunk(
@@ -81,10 +93,19 @@ data class SupplementalContextInfo(
8193
}
8294

8395
data class RecommendationContext(
84-
val details: List<DetailContext>,
96+
val details: MutableList<DetailContext>,
8597
val userInputOriginal: String,
8698
val userInputSinceInvocation: String,
87-
val position: VisualPosition
99+
val position: VisualPosition,
100+
val jobId: Int,
101+
var typeahead: String = "",
102+
)
103+
104+
data class PreviewContext(
105+
val jobId: Int,
106+
val detail: DetailContext,
107+
val userInput: String,
108+
val typeahead: String,
88109
)
89110

90111
data class DetailContext(
@@ -95,17 +116,47 @@ data class DetailContext(
95116
val isTruncatedOnRight: Boolean,
96117
val rightOverlap: String = "",
97118
val completionType: CodewhispererCompletionType,
119+
var hasSeen: Boolean = false,
120+
var isAccepted: Boolean = false
98121
)
99122

100123
data class SessionContext(
101-
val typeahead: String = "",
102-
val typeaheadOriginal: String = "",
103-
val selectedIndex: Int = 0,
124+
val project: Project,
125+
val editor: Editor,
126+
var popup: JBPopup? = null,
127+
var selectedIndex: Int = -1,
104128
val seen: MutableSet<Int> = mutableSetOf(),
105-
val isFirstTimeShowingPopup: Boolean = true,
129+
var isFirstTimeShowingPopup: Boolean = true,
106130
var toBeRemovedHighlighter: RangeHighlighter? = null,
107-
var insertEndOffset: Int = -1
108-
)
131+
var insertEndOffset: Int = -1,
132+
var popupDisplayOffset: Int = -1,
133+
val latencyContext: LatencyContext,
134+
var hasAccepted: Boolean = false
135+
) : Disposable {
136+
private var isDisposed = false
137+
138+
@RequiresEdt
139+
override fun dispose() {
140+
CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll(
141+
this,
142+
hasAccepted,
143+
CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) }
144+
)
145+
CodeWhispererInvocationStatus.getInstance().setDisplaySessionActive(false)
146+
147+
if (hasAccepted) {
148+
popup?.closeOk(null)
149+
} else {
150+
popup?.cancel()
151+
}
152+
popup?.let { Disposer.dispose(it) }
153+
popup = null
154+
CodeWhispererInvocationStatus.getInstance().finishInvocation()
155+
isDisposed = true
156+
}
157+
158+
fun isDisposed() = isDisposed
159+
}
109160

110161
data class RecommendationChunk(
111162
val text: String,
@@ -124,16 +175,21 @@ data class InvocationContext(
124175
val requestContext: RequestContext,
125176
val responseContext: ResponseContext,
126177
val recommendationContext: RecommendationContext,
127-
val popup: JBPopup
128178
) : Disposable {
129-
override fun dispose() {}
179+
private var isDisposed = false
180+
181+
@RequiresEdt
182+
override fun dispose() {
183+
isDisposed = true
184+
}
185+
186+
fun isDisposed() = isDisposed
130187
}
131188

132189
data class WorkerContext(
133190
val requestContext: RequestContext,
134191
val responseContext: ResponseContext,
135192
val response: GenerateCompletionsResponse,
136-
val popup: JBPopup
137193
)
138194

139195
data class CodeScanTelemetryEvent(

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/popup/CodeWhispererPopupListener.kt

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,11 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.popup
55

66
import com.intellij.openapi.ui.popup.JBPopupListener
77
import com.intellij.openapi.ui.popup.LightweightWindowEvent
8-
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
9-
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
10-
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
11-
import java.time.Duration
12-
import java.time.Instant
8+
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
139

14-
class CodeWhispererPopupListener(private val states: InvocationContext) : JBPopupListener {
15-
override fun beforeShown(event: LightweightWindowEvent) {
16-
super.beforeShown(event)
17-
CodeWhispererInvocationStatus.getInstance().setPopupStartTimestamp()
18-
}
10+
class CodeWhispererPopupListener : JBPopupListener {
1911
override fun onClosed(event: LightweightWindowEvent) {
2012
super.onClosed(event)
21-
val (requestContext, responseContext, recommendationContext) = states
22-
23-
CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll(
24-
requestContext,
25-
responseContext,
26-
recommendationContext,
27-
CodeWhispererPopupManager.getInstance().sessionContext,
28-
event.isOk,
29-
CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) }
30-
)
31-
32-
CodeWhispererInvocationStatus.getInstance().setPopupActive(false)
13+
CodeWhispererService.getInstance().disposeDisplaySession(event.isOk)
3314
}
3415
}

0 commit comments

Comments
 (0)