feat: add task summaries and refine rollback panel ui

This commit is contained in:
Carl-Robert Linnupuu 2026-01-15 18:20:27 +00:00
parent 5e65d81751
commit 5c16465c26
8 changed files with 130 additions and 99 deletions

View file

@ -158,7 +158,7 @@ class AgentEventHandler(
currentResponseBody = responseBody
}
override fun onAgentCompleted(ctx: AgentCompletedContext) {
override fun onAgentCompleted(agentId: String) {
runCatching {
project.service<AgentToolWindowContentManager>().getTabbedPane()
.onAgentCompleted(sessionId)
@ -171,7 +171,7 @@ class AgentEventHandler(
.orElse(null)
}.getOrNull()
val resolvedAgentId = project.service<AgentService>().getAgentForSession(sessionId)?.id
?: ctx.agentId
?: agentId
project.service<AgentSessionState>().updateSession(
sessionId,
lastAgentId = resolvedAgentId,

View file

@ -93,12 +93,17 @@ class AgentRunDslPanel(
rowsPanel.removeAll()
finalList.forEach { value ->
val summary = when (value) {
is RunEntry.TaskEntry -> formatTaskSummary(value.summary)
else -> null
}
val descriptor = ToolCallDescriptorFactory.create(
project = project,
toolName = value.toolName,
args = value.args ?: "",
result = value.result,
overrideKind = value.kind
overrideKind = value.kind,
summary = summary
)
val view = ToolCallView(descriptor)
viewByEntryId[value.id] = view
@ -132,4 +137,16 @@ class AgentRunDslPanel(
fun complete(entryId: String, success: Boolean, result: Any?) {
viewByEntryId[entryId]?.complete(success, result)
}
private fun formatTaskSummary(summary: TaskSummary?): String? {
if (summary == null) return null
val parts = mutableListOf<String>()
if (summary.toolCalls > 0) {
parts.add("${summary.toolCalls} calls")
}
if (summary.tokens > 0) {
parts.add("${summary.tokens} tokens")
}
return if (parts.isNotEmpty()) parts.joinToString(" · ") else null
}
}

View file

@ -21,8 +21,8 @@ import ee.carlrobert.codegpt.toolwindow.agent.ui.renderer.ChangeColors
import ee.carlrobert.codegpt.toolwindow.agent.ui.renderer.lineDiffStats
import ee.carlrobert.codegpt.ui.IconActionButton
import kotlinx.coroutines.*
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.GridLayout
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@ -38,13 +38,12 @@ class RollbackPanel(
private val sessionId: String,
private val onRollbackComplete: () -> Unit
) : BorderLayoutPanel() {
private val rollbackService = RollbackService.getInstance(project)
private val titleLabel = JBLabel()
private val timeLabel = JBLabel()
private val summaryPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 6, 2))
private val changesPanel = JPanel()
private val footerPanel = JPanel(GridLayout(1, 2, 8, 0))
private val scrollPane = JScrollPane(changesPanel)
private val rollbackAllLink = createRollbackAllLink()
private val diffStatsCache = ConcurrentHashMap<String, Triple<Int, Int, Int>>()
private val diffDataCache = ConcurrentHashMap<String, RollbackDiffData>()
private val backgroundScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -54,10 +53,8 @@ class RollbackPanel(
}
private fun setupUI() {
val headerPanel = JPanel(FlowLayout(FlowLayout.LEFT, 6, 2)).apply {
val headerPanel = JPanel(FlowLayout(FlowLayout.LEFT, 4, 2)).apply {
isOpaque = false
add(JBLabel(AllIcons.Actions.Diff))
add(JBLabel(AllIcons.General.Add))
add(titleLabel.apply {
font = font.deriveFont(java.awt.Font.BOLD)
})
@ -66,14 +63,10 @@ class RollbackPanel(
})
}
summaryPanel.apply {
isOpaque = false
}
val topPanel = BorderLayoutPanel().apply {
addToLeft(headerPanel)
addToRight(summaryPanel)
border = JBUI.Borders.empty(8)
addToRight(rollbackAllLink)
border = JBUI.Borders.empty(6, 0)
}
changesPanel.apply {
@ -81,18 +74,17 @@ class RollbackPanel(
isOpaque = false
}
footerPanel.apply {
isOpaque = true
border = JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0)
add(createRollbackAllButton())
add(createKeepButton())
scrollPane.apply {
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
border = null
preferredSize = Dimension(0, 240)
}
addToTop(topPanel)
addToCenter(
BorderLayoutPanel().apply {
addToCenter(changesPanel)
addToBottom(footerPanel)
addToCenter(scrollPane)
}
)
border = JBUI.Borders.compound(
@ -110,21 +102,19 @@ class RollbackPanel(
.sortedBy { it.path }
isVisible = changes.isNotEmpty()
if (changes.isEmpty()) {
titleLabel.text = "Agent changes"
titleLabel.text = "Changes"
timeLabel.text = ""
changesPanel.removeAll()
summaryPanel.removeAll()
footerPanel.isVisible = false
rollbackAllLink.isVisible = false
revalidate()
repaint()
return
}
val timeText = snapshot?.completedAt?.let { formatTime(it) } ?: ""
titleLabel.text = "Agent changes (${changes.size})"
titleLabel.text = "Changes (${changes.size})"
timeLabel.text = if (timeText.isNotBlank()) "$timeText" else ""
updateSummary(changes)
preloadDiffStats(changes)
changesPanel.removeAll()
@ -132,7 +122,8 @@ class RollbackPanel(
if (index > 0) changesPanel.add(Box.createVerticalStrut(4))
changesPanel.add(createChangeRow(change))
}
footerPanel.isVisible = true
updateScrollPaneSizing()
rollbackAllLink.isVisible = true
revalidate()
repaint()
@ -220,6 +211,7 @@ class RollbackPanel(
return JBLabel(display).apply {
foreground = JBUI.CurrentTheme.Label.disabledForeground()
font = JBUI.Fonts.smallFont()
toolTipText = display
}
}
@ -287,48 +279,29 @@ class RollbackPanel(
return "Last run at ${formatter.format(instant)}"
}
private fun updateSummary(changes: List<FileChange>) {
val added = changes.count { it.kind == ChangeKind.ADDED }
val deleted = changes.count { it.kind == ChangeKind.DELETED }
val modified =
changes.count { it.kind == ChangeKind.MODIFIED || it.kind == ChangeKind.MOVED }
summaryPanel.removeAll()
if (added > 0) summaryPanel.add(colorChip("+$added", ChangeColors.inserted))
if (deleted > 0) summaryPanel.add(colorChip("-$deleted", ChangeColors.deleted))
if (modified > 0) summaryPanel.add(colorChip("~$modified", ChangeColors.modified))
summaryPanel.revalidate()
summaryPanel.repaint()
}
private fun colorChip(text: String, color: JBColor): JBLabel =
JBLabel(text).apply {
foreground = color
font = JBUI.Fonts.smallFont()
}
private fun createRollbackAllButton(): JComponent {
return JButton("Rollback all").apply {
isFocusable = false
isContentAreaFilled = true
isOpaque = true
putClientProperty("JButton.buttonType", "roundRect")
margin = JBUI.insets(6, 12)
addActionListener { handleRollback() }
}
}
private fun createKeepButton(): JComponent {
return JButton("Keep changes").apply {
isFocusable = false
isContentAreaFilled = true
isOpaque = true
putClientProperty("JButton.buttonType", "roundRect")
margin = JBUI.insets(6, 12)
addActionListener {
rollbackService.clearSnapshot(sessionId)
refreshOperations()
private fun updateScrollPaneSizing() {
val maxVisibleItems = 5
val componentList = changesPanel.components.toList()
var visibleRows = 0
var height = 0
componentList.forEach { component ->
if (visibleRows >= maxVisibleItems) return@forEach
height += component.preferredSize.height
if (component is BorderLayoutPanel) {
visibleRows += 1
}
}
if (height == 0) {
scrollPane.preferredSize = Dimension(0, 0)
return
}
scrollPane.preferredSize = Dimension(0, height)
scrollPane.maximumSize = Dimension(Int.MAX_VALUE, height)
maximumSize = Dimension(Int.MAX_VALUE, preferredSize.height)
}
private fun createRollbackAllLink(): JComponent {
return ActionLink("Rollback all changes") { handleRollback() }
}
private fun rollbackFile(path: String) {

View file

@ -41,5 +41,6 @@ data class ToolCallDescriptor(
val args: Any,
val result: Any? = null,
val projectId: String? = null,
val prefixColor: JBColor? = null
val prefixColor: JBColor? = null,
val summary: String? = null
)

View file

@ -29,7 +29,8 @@ object ToolCallDescriptorFactory {
toolName: String,
args: Any,
result: Any? = null,
overrideKind: ToolKind? = null
overrideKind: ToolKind? = null,
summary: String? = null
): ToolCallDescriptor {
val kind = overrideKind ?: detectToolKind(toolName, args)
val projectId = project.locationHash
@ -41,7 +42,7 @@ object ToolCallDescriptorFactory {
ToolKind.EDIT -> createEditDescriptor(project, args, result, projectId)
ToolKind.BASH -> createBashDescriptor(args, result, projectId)
ToolKind.WEB -> createWebDescriptor(args, result, projectId)
ToolKind.TASK -> createTaskDescriptor(args, result, projectId)
ToolKind.TASK -> createTaskDescriptor(args, result, projectId, summary)
ToolKind.LIBRARY_RESOLVE -> createLibraryResolveDescriptor(args, result, projectId)
ToolKind.LIBRARY_DOCS -> createLibraryDocsDescriptor(args, result, projectId)
ToolKind.ASK_QUESTION -> createAskDescriptor(args, result, projectId)
@ -374,28 +375,12 @@ object ToolCallDescriptorFactory {
val pattern = searchArgs?.pattern ?: ""
val scopeOrPath = searchArgs?.path?.substringAfterLast('/') ?: (searchArgs?.scope ?: "")
val titleMain = buildSearchDisplay(truncatePattern(pattern), scopeOrPath)
val badges = mutableListOf<Badge>()
val actions = mutableListOf<ToolAction>()
when (result) {
is IntelliJSearchTool.Result -> {
badges.add(Badge("${result.totalMatches} matches", JBColor.BLUE))
if (result.output.isNotBlank()) {
actions.add(
ToolAction("View Results", AllIcons.Actions.Find) { _ ->
showTextDialog(result.output, "Search Results")
}
)
}
}
is String -> {
actions.add(
ToolAction("View Results", AllIcons.Actions.Find) { _ ->
showTextDialog(result, "Search Results")
}
)
}
}
@ -442,7 +427,8 @@ object ToolCallDescriptorFactory {
private fun createTaskDescriptor(
args: Any,
result: Any?,
projectId: String?
projectId: String?,
summary: String? = null
): ToolCallDescriptor {
val description = when (args) {
is TaskTool.Args -> args.description
@ -463,6 +449,12 @@ object ToolCallDescriptorFactory {
prefixColor = null
}
val taskSummary = when {
summary != null -> summary
result is TaskTool.Result -> formatTaskSummary(result)
else -> null
}
return ToolCallDescriptor(
kind = ToolKind.TASK,
icon = AllIcons.Actions.RunAnything,
@ -474,10 +466,30 @@ object ToolCallDescriptorFactory {
args = args,
result = result,
projectId = projectId,
prefixColor = prefixColor
prefixColor = prefixColor,
summary = taskSummary
)
}
private fun formatTaskSummary(result: TaskTool.Result): String? {
val parts = mutableListOf<String>()
if (result.executionTime > 0) {
parts.add(formatDuration(result.executionTime))
}
if (result.totalTokens > 0) {
parts.add("${result.totalTokens} tokens")
}
return if (parts.isNotEmpty()) parts.joinToString(" · ") else null
}
private fun formatDuration(ms: Long): String {
return when {
ms < 1000 -> "${ms}ms"
ms < 60000 -> "${ms / 1000}s"
else -> "${ms / 60000}m ${((ms % 60000) / 1000)}s"
}
}
private fun getSubagentColor(subagentType: String): JBColor {
val hue = subagentType.hashCode().absoluteValue % 360
val hueNormalized = hue.toFloat() / 360f

View file

@ -120,6 +120,12 @@ private class ToolCallHeaderPanel(
this.fileLink = link
leftRow.add(link)
descriptor.summary?.let { summary ->
leftRow.add(JBLabel(" · $summary").withFont(JBFont.small()).apply {
foreground = JBUI.CurrentTheme.Label.disabledForeground()
})
}
}
private fun addRegularContent() {
@ -129,7 +135,12 @@ private class ToolCallHeaderPanel(
}
leftRow.add(content)
// Show IntelliJSearch parameter chips for compact rows for parity with main cards
descriptor.summary?.let { summary ->
leftRow.add(JBLabel(" · $summary").withFont(JBFont.small()).apply {
foreground = JBUI.CurrentTheme.Label.disabledForeground()
})
}
addSearchParametersIfAny()
}

View file

@ -41,12 +41,30 @@ private fun longestCommonSubsequenceLength(a: List<String>, b: List<String>): In
val n = a.size
val m = b.size
if (n == 0 || m == 0) return 0
val dp = Array(n + 1) { IntArray(m + 1) }
for (i in 1..n) {
val ai = a[i - 1]
for (j in 1..m) {
dp[i][j] = if (ai == b[j - 1]) dp[i - 1][j - 1] + 1 else maxOf(dp[i - 1][j], dp[i][j - 1])
val smaller = if (n < m) a else b
val larger = if (n < m) b else a
val smallSize = smaller.size
val largeSize = larger.size
var prev = IntArray(smallSize + 1)
var curr = IntArray(smallSize + 1)
for (i in 1..largeSize) {
val largerLine = larger[i - 1]
val temp = prev
prev = curr
curr = temp
for (j in 1..smallSize) {
curr[j] = if (largerLine == smaller[j - 1]) {
prev[j - 1] + 1
} else {
maxOf(prev[j], curr[j - 1])
}
}
}
return dp[n][m]
return curr[smallSize]
}

View file

@ -87,7 +87,6 @@ The user will primarily request you perform software engineering tasks. This inc
- When doing file search, prefer to use the Task tool in order to reduce context usage.
- You should proactively use the Task tool with specialized agents when the task at hand matches the agent's description.
- When WebFetch returns a message about a redirect to a different host, you should immediately make a new WebFetch request with the redirect URL provided in the response.
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Task tool with subagent_type=Explore instead of running search commands directly.
@ -100,7 +99,7 @@ user: What is the codebase structure?
assistant: [Uses the Task tool with subagent_type=Explore]
</example>
You can use the following tools without requiring user approval: Bash(curl:*), Bash(chmod:*), Bash(docker-compose restart:*), Bash(docker-compose down:*), Bash(docker-compose up:*), Bash(docker-compose:*), Bash(find:*), WebFetch(domain:medium.com), Bash(cp:*), Bash(mv:*), Bash(mkdir:*), Bash(touch:*), Bash(ls:*), Bash(./gradlew build:*), Bash(./gradlew:*), Bash(true), Bash(cat:*), Bash(diff:*), Bash(rm:*), WebSearch, BashOutput, KillShell
You can use the following tools without requiring user approval: Bash(curl:*), Bash(chmod:*), Bash(docker-compose restart:*), Bash(docker-compose down:*), Bash(docker-compose up:*), Bash(docker-compose:*), Bash(find:*), Bash(cp:*), Bash(mv:*), Bash(mkdir:*), Bash(touch:*), Bash(ls:*), Bash(./gradlew build:*), Bash(./gradlew:*), Bash(true), Bash(cat:*), Bash(diff:*), Bash(rm:*), WebSearch, BashOutput, KillShell
Here is useful information about the environment you are running in:
<env>