diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt index cc0111d5..9c39f15f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/AgentEventHandler.kt @@ -158,7 +158,7 @@ class AgentEventHandler( currentResponseBody = responseBody } - override fun onAgentCompleted(ctx: AgentCompletedContext) { + override fun onAgentCompleted(agentId: String) { runCatching { project.service().getTabbedPane() .onAgentCompleted(sessionId) @@ -171,7 +171,7 @@ class AgentEventHandler( .orElse(null) }.getOrNull() val resolvedAgentId = project.service().getAgentForSession(sessionId)?.id - ?: ctx.agentId + ?: agentId project.service().updateSession( sessionId, lastAgentId = resolvedAgentId, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentRunDslPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentRunDslPanel.kt index 15242351..57a71d2a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentRunDslPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/AgentRunDslPanel.kt @@ -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() + 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 + } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/RollbackPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/RollbackPanel.kt index d2274437..86a50087 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/RollbackPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/RollbackPanel.kt @@ -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>() private val diffDataCache = ConcurrentHashMap() 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) { - 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) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptor.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptor.kt index 4c7a2b84..c62aba74 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptor.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptor.kt @@ -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 ) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptorFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptorFactory.kt index a9656d27..aac5bcfe 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptorFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallDescriptorFactory.kt @@ -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() val actions = mutableListOf() 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() + 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 diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallView.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallView.kt index 9f185212..00c6b70a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallView.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/descriptor/ToolCallView.kt @@ -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() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/renderer/SimpleChangeUtils.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/renderer/SimpleChangeUtils.kt index 1835c562..a47a9359 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/renderer/SimpleChangeUtils.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/agent/ui/renderer/SimpleChangeUtils.kt @@ -41,12 +41,30 @@ private fun longestCommonSubsequenceLength(a: List, b: List): 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] } diff --git a/src/main/resources/prompts/agent/anthropic.txt b/src/main/resources/prompts/agent/anthropic.txt index 937824f3..03c315d9 100644 --- a/src/main/resources/prompts/agent/anthropic.txt +++ b/src/main/resources/prompts/agent/anthropic.txt @@ -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] -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: