From 6673ba4b2b87d2de32d494239bc57fa487c742cc Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sun, 15 Dec 2024 17:42:20 -0500 Subject: [PATCH] find results edit - initial version --- .../find/FindResultsModificationAction.kt | 77 ++++++++++++--- .../find/FindResultsModificationDialog.kt | 53 +++++++---- .../actions/generic/MultiDiffChatAction.kt | 93 ++++++++++--------- 3 files changed, 143 insertions(+), 80 deletions(-) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationAction.kt index 2ba090c..6c9cdf5 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationAction.kt @@ -2,25 +2,28 @@ package com.github.simiacryptus.aicoder.actions import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.find.FindResultsModificationDialog +import com.github.simiacryptus.aicoder.actions.generic.MultiDiffChatAction.Companion.patchEditorPrompt import com.github.simiacryptus.aicoder.actions.generic.SessionProxyServer import com.github.simiacryptus.aicoder.actions.generic.toFile +import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.UITools import com.github.simiacryptus.aicoder.util.psi.PsiUtil import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.findPsiFile import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import com.intellij.usages.Usage import com.intellij.usages.UsageView +import com.simiacryptus.diff.addApplyFileDiffLinks +import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.SimpleActor import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.model.User import com.simiacryptus.skyenet.core.util.getModuleRootForFile -import com.simiacryptus.skyenet.util.MarkdownUtil +import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown import com.simiacryptus.skyenet.webui.application.AppInfoData import com.simiacryptus.skyenet.webui.application.ApplicationServer import com.simiacryptus.skyenet.webui.application.ApplicationSocketManager @@ -28,6 +31,7 @@ import com.simiacryptus.skyenet.webui.session.SocketManager import java.io.File import java.nio.file.Path import java.text.SimpleDateFormat +import java.util.* import javax.swing.Icon class FindResultsModificationAction( @@ -99,23 +103,40 @@ class FindResultsModificationAction( path = "/patchChat", showMenubar = false, ) { + override val singleInput = true + override val stickyInput = false + override fun newSession(user: User?, session: Session): SocketManager { val socketManager = super.newSession(user, session) val ui = (socketManager as ApplicationSocketManager).applicationInterface val task = ui.newTask() + val api = api.getChildClient().apply { + val createFile = task.createFile(".logs/api-${UUID.randomUUID()}.log") + createFile.second?.apply { + logStreams += this.outputStream().buffered() + task.verbose("API log: $this") + } + } val tabs = TabbedDisplay(task) usages.groupBy { it.location?.editor?.file }.entries.forEach { (file, usages) -> val task = ui.newTask(false) tabs[if (null == file) "Unknown" else file.name] = task.placeholder + val api = api.getChildClient().apply { + val createFile = task.createFile(".logs/api-${UUID.randomUUID()}.log") + createFile.second?.apply { + logStreams += this.outputStream().buffered() + task.verbose("API log: $this") + } + } fun formatLine(index: Int, line: String, isFocused: Boolean) = when { isFocused -> "/* L$index */ $line /* <<< */" else -> "/* L$index */ $line" } - val document = getDocument(project, file ?: return@forEach) ?: return@forEach + val document = PsiDocumentManager.getInstance(project).getDocument(file?.findPsiFile(project) ?: return@forEach) ?: return@forEach val psiRoot: PsiFile? = file.findPsiFile(project) val byContainer = usages.groupBy { getSmallestContainingEntity(psiRoot, it) }.entries.sortedBy { it.key?.textRange?.startOffset }.toTypedArray() val lines = document.text.lines() - val filteredLines = lines.mapIndexedNotNull { index: Int, line: String -> + val filteredLines = lines.mapIndexed { index: Int, line: String -> val lineStart = lines.subList(0, index).joinToString("\n").length val lineEnd = lineStart + line.length val containers = byContainer.map { it.key }.filter { @@ -132,11 +153,39 @@ class FindResultsModificationAction( } else if (containers.isNotEmpty()) { formatLine(index, line, false) } else { - null + "..." } - }.joinToString("\n") - task.add(MarkdownUtil.renderMarkdown("## ${file.name}\n\n```${file.extension}\n$filteredLines\n```\n")) - // TODO: Ask simpleagent for change, and instrument diffs + }.joinToString("\n").replace("(?:\\.\\.\\.\n){2,}".toRegex(), "...\n") + val fileListingMarkdown = "## ${file.name}\n\n```${file.extension}\n$filteredLines\n```\n" + task.add(renderMarkdown(fileListingMarkdown)) + val prompt = """ + You are a code modification assistant. You will receive code files and locations where changes are needed. + Your task is to suggest appropriate modifications based on the replacement text provided. + Usage locations: + """.trimIndent() + usages.joinToString("\n") { "* `${it.presentation.plainText}`" } + + "\n\nRequested modification: " + modificationParams.replacementText + "\n\n" + patchEditorPrompt + ui.socketManager?.addApplyFileDiffLinks( + root = root.toPath(), + response = SimpleActor( + prompt = prompt, + model = AppSettingsState.instance.smartModel.chatModel() + ).answer( + listOf( + fileListingMarkdown + ), api + ).replace(Regex("""/\* L\d+ \*/"""), "").replace(Regex("""/\* <<< \*/"""), ""), + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + task.complete("Updated $path") + } + }, + ui = ui, + api = api, + shouldAutoApply = { modificationParams.autoApply }, + defaultFile = file.toFile.path + )?.apply { + task.complete(renderMarkdown(this)) + } } return socketManager } @@ -149,10 +198,6 @@ class FindResultsModificationAction( ) } - private fun getDocument(project: Project, file: VirtualFile): Document? { - return PsiDocumentManager.getInstance(project).getDocument(file.findPsiFile(project) ?: return null) - } - override fun isEnabled(event: AnActionEvent): Boolean { val usageView = event.getData(UsageView.USAGE_VIEW_KEY) return usageView != null && usageView.usages.isNotEmpty() @@ -163,13 +208,15 @@ class FindResultsModificationAction( val config = dialog.showAndGetConfig() return if (config != null) { ModificationParams( - config.replacementText ?: "" + replacementText = config.replacementText ?: "", + autoApply = config.autoApply ) } else null } data class ModificationParams( - val replacementText: String + val replacementText: String, + val autoApply: Boolean ) } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationDialog.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationDialog.kt index 16180d9..38ded65 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationDialog.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/find/FindResultsModificationDialog.kt @@ -3,10 +3,8 @@ package com.github.simiacryptus.aicoder.actions.find import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBScrollPane -import com.intellij.ui.components.JBTextArea -import com.intellij.util.ui.FormBuilder +import com.intellij.ui.dsl.builder.* +import com.intellij.ui.dsl.gridLayout.HorizontalAlign import javax.swing.JComponent class FindResultsModificationDialog( @@ -14,41 +12,58 @@ class FindResultsModificationDialog( matchCount: Int ) : DialogWrapper(project) { - private val replacementTextArea = JBTextArea().apply { - rows = 5 - lineWrap = true - wrapStyleWord = true - text = "Please modify this code to:" - } - private val matchCountLabel = JBLabel("Found $matchCount matches") + private var replacementText = "Please modify this code to: " + private var autoApply = false init { title = "AI-Based Find Results Modification" + setOKButtonText("Modify Code") init() } override fun createCenterPanel(): JComponent { - val formBuilder = FormBuilder.createFormBuilder() - formBuilder.addComponent(matchCountLabel) - formBuilder.addLabeledComponent("AI Instructions:", JBScrollPane(replacementTextArea)) - return formBuilder.panel + return panel { + row("Modification Instructions:") { + textArea() + .bindText({ replacementText }, { replacementText = it }) + .rows(5) + .align(Align.FILL) + .comment("Enter instructions for how you want the code to be modified") + .focused() + .apply { + component.lineWrap = true + component.wrapStyleWord = true + component.selectAll() + } + }.resizableRow() + row { + checkBox("Auto-apply changes") + .bindSelected({ autoApply }, { autoApply = it }) + .comment("Automatically apply changes without manual confirmation") + } + } } override fun doValidate(): ValidationInfo? { - if (replacementTextArea.text.isBlank()) { - return ValidationInfo("Replacement text cannot be empty", replacementTextArea) + if (replacementText.isBlank()) { + return ValidationInfo("Please enter instructions for code modification") + } + if (replacementText.length < 10) { + return ValidationInfo("Please provide more detailed instructions") } return null } data class ConfigData( - val replacementText: String? + val replacementText: String?, + val autoApply: Boolean ) fun showAndGetConfig(): ConfigData? { if (showAndGet()) { return ConfigData( - replacementText = replacementTextArea.text + replacementText = replacementText, + autoApply = autoApply ) } return null diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt index c3e7afc..4588060 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt @@ -134,57 +134,22 @@ class MultiDiffChatAction : BaseAction() { api: API ) { try { - fun mainActor() = SimpleActor( - prompt = """ - You are a helpful AI that helps people with coding. - - You will be answering questions about the following code: - - """.trimIndent() + codeSummary() + """ - - Response should use one or more code patches in diff format within ```diff code blocks. - Each diff should be preceded by a header that identifies the file being modified. - The diff format should use + for line additions, - for line deletions. - The diff should include 2 lines of context before and after every change. - - Example: - - Here are the patches: - - ### src/utils/exampleUtils.js - ```diff - // Utility functions for example feature - const b = 2; - function exampleFunction() { - - return b + 1; - + return b + 2; - } - ``` - - ### tests/exampleUtils.test.js - ```diff - // Unit tests for exampleUtils - const assert = require('assert'); - const { exampleFunction } = require('../src/utils/exampleUtils'); - - describe('exampleFunction', () => { - - it('should return 3', () => { - + it('should return 4', () => { - assert.equal(exampleFunction(), 3); - }); - }); - ``` - - If needed, new files can be created by using code blocks labeled with the filename in the same manner. - """.trimIndent(), - model = AppSettingsState.instance.smartModel.chatModel() - ) + fun mainActor(): SimpleActor { + return SimpleActor( + prompt = """ + You are a helpful AI that helps people with coding. + + You will be answering questions about the following code: + + """.trimIndent() + codeSummary() + patchEditorPrompt, + model = AppSettingsState.instance.smartModel.chatModel() + ) + } val settings = getSettings(session, user) ?: Settings() if (api is ChatClient) api.budget = settings.budget ?: 2.00 val task = ui.newTask() -// Add progress indication task.add("Processing request...") val api = (api as ChatClient).getChildClient().apply { @@ -261,6 +226,42 @@ class MultiDiffChatAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(MultiDiffChatAction::class.java) + val patchEditorPrompt = """ + Response should use one or more code patches in diff format within ```diff code blocks. + Each diff should be preceded by a header that identifies the file being modified. + The diff format should use + for line additions, - for line deletions. + The diff should include 2 lines of context before and after every change. + + Example: + + Here are the patches: + + ### src/utils/exampleUtils.js + ```diff + // Utility functions for example feature + const b = 2; + function exampleFunction() { + - return b + 1; + + return b + 2; + } + ``` + + ### tests/exampleUtils.test.js + ```diff + // Unit tests for exampleUtils + const assert = require('assert'); + const { exampleFunction } = require('../src/utils/exampleUtils'); + + describe('exampleFunction', () => { + - it('should return 3', () => { + + it('should return 4', () => { + assert.equal(exampleFunction(), 3); + }); + }); + ``` + + If needed, new files can be created by using code blocks labeled with the filename in the same manner. + """.trimIndent() } }