mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 15:52:01 +07:00
[ghai] Add a way to generate a summary of a PR with AI
Add actions: - Generate a summary - Like the generated summary - Dislike the generated summary Adds UI: - A comment in timeline with a purple hover color - buttons for above actions (cherry picked from commit 6f734fb94f53a4efd8dd950b51d427a21f9ab117) (cherry picked from commit 3e0ae846aa29182724b7a350a2567bff89070351) IJ-CR-148445 GitOrigin-RevId: 51e1544114d334edfc5f001f9145e6d4a0c7e0ae
This commit is contained in:
committed by
intellij-monorepo-bot
parent
bc555ebd94
commit
ae3fd256fb
@@ -399,14 +399,16 @@ f:com.intellij.collaboration.api.json.JsonHttpApiHelperKt
|
||||
- f:build(com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil$ComponentType,kotlin.jvm.functions.Function1,javax.swing.JComponent,kotlin.jvm.functions.Function1):javax.swing.JComponent
|
||||
- f:buildDynamic(com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil$ComponentType,kotlin.jvm.functions.Function1,javax.swing.JComponent,kotlin.jvm.functions.Function1):javax.swing.JComponent
|
||||
- f:getTEXT_CONTENT_WIDTH():I
|
||||
- f:withHoverHighlight(javax.swing.JComponent):javax.swing.JComponent
|
||||
- f:withHoverHighlight(javax.swing.JComponent,com.intellij.ui.JBColor):javax.swing.JComponent
|
||||
*f:com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil$Builder
|
||||
- <init>(com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil$ComponentType,kotlin.jvm.functions.Function1,javax.swing.JComponent):V
|
||||
- f:build():javax.swing.JComponent
|
||||
- f:getHeader():com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil$HeaderComponents
|
||||
- f:getHoverHighlight():com.intellij.ui.JBColor
|
||||
- f:getIconTooltip():java.lang.String
|
||||
- f:getMaxContentWidth():java.lang.Integer
|
||||
- f:setHeader(com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil$HeaderComponents):V
|
||||
- f:setHoverHighlight(com.intellij.ui.JBColor):V
|
||||
- f:setIconTooltip(java.lang.String):V
|
||||
- f:setMaxContentWidth(java.lang.Integer):V
|
||||
- f:withHeader(javax.swing.JComponent,javax.swing.JComponent):com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil$Builder
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.intellij.collaboration.ui.VerticalListPanel
|
||||
import com.intellij.collaboration.ui.codereview.avatar.Avatar
|
||||
import com.intellij.collaboration.ui.codereview.comment.CodeReviewCommentUIUtil
|
||||
import com.intellij.collaboration.ui.util.CodeReviewColorUtil
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.hover.HoverStateListener
|
||||
import com.intellij.ui.scale.JBUIScale
|
||||
import com.intellij.util.ui.JBUI
|
||||
@@ -76,8 +77,8 @@ object CodeReviewChatItemUIUtil {
|
||||
SUPER_COMPACT {
|
||||
override val iconSize: Int = Avatar.Sizes.BASE
|
||||
override val iconGap: Int = 10
|
||||
override val paddingInsets: Insets = Insets(0,0,0,0)
|
||||
override val inputPaddingInsets: Insets = Insets(0,0,0,0)
|
||||
override val paddingInsets: Insets = Insets(0, 0, 0, 0)
|
||||
override val inputPaddingInsets: Insets = Insets(0, 0, 0, 0)
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -113,16 +114,20 @@ object CodeReviewChatItemUIUtil {
|
||||
get() = iconSize + iconGap
|
||||
}
|
||||
|
||||
fun build(type: ComponentType,
|
||||
iconProvider: (iconSize: Int) -> Icon,
|
||||
content: JComponent,
|
||||
init: Builder.() -> Unit): JComponent =
|
||||
fun build(
|
||||
type: ComponentType,
|
||||
iconProvider: (iconSize: Int) -> Icon,
|
||||
content: JComponent,
|
||||
init: Builder.() -> Unit,
|
||||
): JComponent =
|
||||
buildDynamic(type, { iconSize -> SingleValueModel(iconProvider(iconSize)) }, content, init)
|
||||
|
||||
fun buildDynamic(type: ComponentType,
|
||||
iconValueProvider: (iconSize: Int) -> SingleValueModel<Icon>,
|
||||
content: JComponent,
|
||||
init: Builder.() -> Unit): JComponent =
|
||||
fun buildDynamic(
|
||||
type: ComponentType,
|
||||
iconValueProvider: (iconSize: Int) -> SingleValueModel<Icon>,
|
||||
content: JComponent,
|
||||
init: Builder.() -> Unit,
|
||||
): JComponent =
|
||||
Builder(type, iconValueProvider, content).apply(init).build()
|
||||
|
||||
/**
|
||||
@@ -131,7 +136,7 @@ object CodeReviewChatItemUIUtil {
|
||||
class Builder(
|
||||
private val type: ComponentType,
|
||||
private val iconValueProvider: (Int) -> SingleValueModel<Icon>,
|
||||
private val content: JComponent
|
||||
private val content: JComponent,
|
||||
) {
|
||||
/**
|
||||
* Tooltip for a main icon
|
||||
@@ -149,6 +154,11 @@ object CodeReviewChatItemUIUtil {
|
||||
*/
|
||||
var header: HeaderComponents? = null
|
||||
|
||||
/**
|
||||
* The color to use as the background color on-hover.
|
||||
*/
|
||||
var hoverHighlight: JBColor = CodeReviewColorUtil.Review.Chat.hover
|
||||
|
||||
/**
|
||||
* Helper fun to setup [HeaderComponents]
|
||||
*/
|
||||
@@ -175,7 +185,9 @@ object CodeReviewChatItemUIUtil {
|
||||
actionsVisibleOnHover(it, header?.actions)
|
||||
}.apply {
|
||||
border = JBUI.Borders.empty(type.paddingInsets)
|
||||
}.let { withHoverHighlight(it) }
|
||||
}.let {
|
||||
withHoverHighlight(it, hoverHighlight)
|
||||
}
|
||||
|
||||
private fun <T> JComponent.wrapIfNotNull(value: T?, block: (JComponent, T) -> JComponent): JComponent = let {
|
||||
if (value != null) block(it, value) else it
|
||||
@@ -218,7 +230,7 @@ object CodeReviewChatItemUIUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun withHoverHighlight(comp: JComponent): JComponent {
|
||||
fun withHoverHighlight(comp: JComponent, hoverHighlight: JBColor): JComponent {
|
||||
val highlighterPanel = JPanelWithBackground(BorderLayout()).apply {
|
||||
isOpaque = false
|
||||
background = null
|
||||
@@ -228,7 +240,7 @@ object CodeReviewChatItemUIUtil {
|
||||
override fun hoverChanged(component: Component, hovered: Boolean) {
|
||||
// TODO: extract to theme colors
|
||||
component.background = if (hovered) {
|
||||
CodeReviewColorUtil.Review.Chat.hover
|
||||
hoverHighlight
|
||||
}
|
||||
else {
|
||||
null
|
||||
|
||||
@@ -321,6 +321,18 @@ fun <D> Wrapper.bindContentIn(
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
fun Wrapper.bindContent(
|
||||
debugName: String, contentFlow: Flow<JComponent?>,
|
||||
) {
|
||||
showingScope(debugName) {
|
||||
contentFlow.collect {
|
||||
setContent(it)
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <D> Wrapper.bindContentImpl(
|
||||
dataFlow: Flow<D>,
|
||||
componentFactory: CoroutineScope.(D) -> JComponent?,
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
interface="org.jetbrains.plugins.github.ai.GHPRAIReviewExtension"
|
||||
dynamic="true"/>
|
||||
|
||||
<extensionPoint qualifiedName="intellij.vcs.github.aiSummaryExtension"
|
||||
interface="org.jetbrains.plugins.github.ai.GHPRAISummaryExtension"
|
||||
dynamic="true"/>
|
||||
|
||||
<extensionPoint qualifiedName="com.intellij.vcs.github.gistContentsCollector"
|
||||
interface="org.jetbrains.plugins.github.GithubGistContentsCollector"
|
||||
dynamic="true"/>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.plugins.github.ai
|
||||
|
||||
import com.intellij.collaboration.async.singleExtensionFlow
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.project.Project
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRDataContext
|
||||
import org.jetbrains.plugins.github.pullrequest.data.provider.GHPRDataProvider
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Represents a summary that can be generated at the top of the timeline.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
interface GHPRAISummaryViewModel {
|
||||
val isGenerating: StateFlow<Boolean>
|
||||
|
||||
val summaryHtml: StateFlow<GenerationState<String>>
|
||||
|
||||
val rating: StateFlow<Boolean?>
|
||||
val hasKnownActivity: StateFlow<Boolean>
|
||||
|
||||
fun startGenerating()
|
||||
fun stopGenerating()
|
||||
|
||||
fun rate(isUseful: Boolean)
|
||||
|
||||
sealed interface GenerationState<out T> {
|
||||
data object NotStarted : GenerationState<Nothing>
|
||||
data class NotStartedDueToError(val error: @Nls String) : GenerationState<Nothing>
|
||||
data object Loading : GenerationState<Nothing>
|
||||
data class Error(val error: Exception) : GenerationState<Nothing>
|
||||
|
||||
sealed interface WithValue<T> : GenerationState<T> {
|
||||
val value: T
|
||||
}
|
||||
|
||||
data class Step<T>(override val value: T) : WithValue<T>
|
||||
data class Interrupted<T>(override val value: T) : WithValue<T>
|
||||
data class Done<T>(override val value: T) : WithValue<T>
|
||||
|
||||
fun getOrNull(): T? = (this as? WithValue)?.value
|
||||
fun isLoading(): Boolean = this is Loading || this is Step
|
||||
|
||||
fun <R> map(mapper: (T) -> R): GenerationState<R> = when (this) {
|
||||
is Step -> Step(mapper(value))
|
||||
is Interrupted -> Interrupted(mapper(value))
|
||||
is Done -> Done(mapper(value))
|
||||
is NotStarted -> this
|
||||
is NotStartedDueToError -> this
|
||||
is Loading -> this
|
||||
is Error -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface GHPRAISummaryExtension {
|
||||
companion object {
|
||||
private val EP = ExtensionPointName.create<GHPRAISummaryExtension>("intellij.vcs.github.aiSummaryExtension")
|
||||
|
||||
internal val singleFlow: Flow<GHPRAISummaryExtension?>
|
||||
get() = EP.singleExtensionFlow()
|
||||
}
|
||||
|
||||
fun provideSummaryVm(
|
||||
cs: CoroutineScope,
|
||||
project: Project,
|
||||
dataContext: GHPRDataContext,
|
||||
dataProvider: GHPRDataProvider,
|
||||
): GHPRAISummaryViewModel?
|
||||
|
||||
fun createTimelineComponent(vm: GHPRAISummaryViewModel): JComponent
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.plugins.github.ai.GHPRAIReviewExtension
|
||||
import org.jetbrains.plugins.github.ai.GHPRAIReviewViewModel
|
||||
import org.jetbrains.plugins.github.ai.GHPRAISummaryExtension
|
||||
import org.jetbrains.plugins.github.ai.GHPRAISummaryViewModel
|
||||
import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsProjectUISettings
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRDataContext
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRIdentifier
|
||||
@@ -36,6 +38,7 @@ import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.model.GHPRToolWind
|
||||
@ApiStatus.Internal
|
||||
interface GHPRViewModelContainer {
|
||||
val aiReviewVm: StateFlow<GHPRAIReviewViewModel?>
|
||||
val aiSummaryVm: StateFlow<GHPRAISummaryViewModel?>
|
||||
|
||||
val infoVm: GHPRInfoViewModel
|
||||
val branchWidgetVm: GHPRBranchWidgetViewModel
|
||||
@@ -51,7 +54,7 @@ internal class GHPRViewModelContainerImpl(
|
||||
dataContext: GHPRDataContext,
|
||||
private val projectVm: GHPRToolWindowProjectViewModel,
|
||||
private val pullRequestId: GHPRIdentifier,
|
||||
cancelWith: Disposable
|
||||
cancelWith: Disposable,
|
||||
) : GHPRViewModelContainer {
|
||||
private val cs = parentCs.childScope(javaClass.name).cancelledWith(cancelWith)
|
||||
|
||||
@@ -71,6 +74,13 @@ internal class GHPRViewModelContainerImpl(
|
||||
GHPRAIReviewExtension.EP.singleExtensionFlow()
|
||||
.mapScoped { it?.provideReviewVm(project, this, dataContext, dataProvider) }
|
||||
.stateIn(cs, SharingStarted.Eagerly, null)
|
||||
override val aiSummaryVm: StateFlow<GHPRAISummaryViewModel?> =
|
||||
GHPRAISummaryExtension.singleFlow
|
||||
.mapScoped { extension ->
|
||||
val cs = this@mapScoped
|
||||
extension?.provideSummaryVm(cs, project, dataContext, dataProvider)
|
||||
}
|
||||
.stateIn(cs, SharingStarted.Eagerly, null)
|
||||
|
||||
private val branchStateVm by lazy {
|
||||
GHPRReviewBranchStateSharedViewModel(cs, dataContext, dataProvider)
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.intellij.collaboration.ui.codereview.comment.submitActionIn
|
||||
import com.intellij.collaboration.ui.codereview.list.error.ErrorStatusPanelFactory
|
||||
import com.intellij.collaboration.ui.codereview.list.error.ErrorStatusPresenter
|
||||
import com.intellij.collaboration.ui.codereview.timeline.comment.CommentTextFieldFactory
|
||||
import com.intellij.collaboration.ui.util.bindContent
|
||||
import com.intellij.collaboration.ui.util.bindTextHtmlIn
|
||||
import com.intellij.collaboration.ui.util.bindTextIn
|
||||
import com.intellij.collaboration.ui.util.bindVisibilityIn
|
||||
@@ -38,6 +39,7 @@ import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.update.UiNotifyConnector
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.plugins.github.ai.GHPRAISummaryExtension
|
||||
import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException
|
||||
import org.jetbrains.plugins.github.i18n.GithubBundle
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.details.model.GHPRDetailsFull
|
||||
@@ -52,11 +54,13 @@ import javax.swing.JPanel
|
||||
import javax.swing.event.ChangeEvent
|
||||
import javax.swing.event.ChangeListener
|
||||
|
||||
internal class GHPRFileEditorComponentFactory(private val cs: CoroutineScope,
|
||||
private val project: Project,
|
||||
private val projectVm: GHPRToolWindowProjectViewModel,
|
||||
private val timelineVm: GHPRTimelineViewModel,
|
||||
private val initialDetails: GHPRDetailsFull) {
|
||||
internal class GHPRFileEditorComponentFactory(
|
||||
private val cs: CoroutineScope,
|
||||
private val project: Project,
|
||||
private val projectVm: GHPRToolWindowProjectViewModel,
|
||||
private val timelineVm: GHPRTimelineViewModel,
|
||||
private val initialDetails: GHPRDetailsFull,
|
||||
) {
|
||||
|
||||
private val uiDisposable = cs.nestedDisposable()
|
||||
|
||||
@@ -99,6 +103,18 @@ internal class GHPRFileEditorComponentFactory(private val cs: CoroutineScope,
|
||||
border = JBUI.Borders.empty(CodeReviewTimelineUIUtil.VERT_PADDING, 0)
|
||||
|
||||
add(header)
|
||||
|
||||
add(Wrapper().apply {
|
||||
val summaryComponent = combine(
|
||||
projectVm.acquireAISummaryViewModel(loadedDetails.value.id, uiDisposable),
|
||||
GHPRAISummaryExtension.singleFlow
|
||||
) { summaryVm, extension ->
|
||||
summaryVm?.let { extension?.createTimelineComponent(it) }
|
||||
}
|
||||
bindVisibilityIn(cs, summaryComponent.map { it != null })
|
||||
bindContent("${javaClass.name}.summaryComponent.content", summaryComponent)
|
||||
})
|
||||
|
||||
add(description)
|
||||
add(timeline)
|
||||
|
||||
@@ -126,7 +142,8 @@ internal class GHPRFileEditorComponentFactory(private val cs: CoroutineScope,
|
||||
}
|
||||
})
|
||||
}
|
||||
UiNotifyConnector.doWhenFirstShown(scrollPane) {
|
||||
UiNotifyConnector.doWhenFirstShown(scrollPane)
|
||||
{
|
||||
timelineVm.requestMore()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.plugins.github.ai.GHPRAIReviewViewModel
|
||||
import org.jetbrains.plugins.github.ai.GHPRAISummaryViewModel
|
||||
import org.jetbrains.plugins.github.api.GHRepositoryConnection
|
||||
import org.jetbrains.plugins.github.api.GHRepositoryCoordinates
|
||||
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequestShort
|
||||
@@ -55,7 +56,7 @@ class GHPRToolWindowProjectViewModel internal constructor(
|
||||
private val project: Project,
|
||||
parentCs: CoroutineScope,
|
||||
private val twVm: GHPRToolWindowViewModel,
|
||||
connection: GHRepositoryConnection
|
||||
connection: GHRepositoryConnection,
|
||||
) : ReviewToolwindowProjectViewModel<GHPRToolWindowTab, GHPRToolWindowTabViewModel> {
|
||||
private val cs = parentCs.childScope(javaClass.name)
|
||||
|
||||
@@ -161,6 +162,9 @@ class GHPRToolWindowProjectViewModel internal constructor(
|
||||
fun acquireAIReviewViewModel(id: GHPRIdentifier, disposable: Disposable): StateFlow<GHPRAIReviewViewModel?> =
|
||||
pullRequestsVms[id].acquireValue(disposable).aiReviewVm
|
||||
|
||||
fun acquireAISummaryViewModel(id: GHPRIdentifier, disposable: Disposable): StateFlow<GHPRAISummaryViewModel?> =
|
||||
pullRequestsVms[id].acquireValue(disposable).aiSummaryVm
|
||||
|
||||
fun acquireInfoViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRInfoViewModel =
|
||||
pullRequestsVms[id].acquireValue(disposable).infoVm
|
||||
|
||||
|
||||
Reference in New Issue
Block a user