diff --git a/CHANGELOG.md b/CHANGELOG.md index e652ce97..e3255ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - VCS menu integration with AI Coder options in the VCS log context menu ### Changed -- Updated dependencies: skyenet_version to 1.0.80 and jo-penai to 1.0.67 +- Updated dependencies: skyenet_version to 1.0.80 and jo-penai to 1.0.68 - Removed kotlinx-coroutines-core dependency - Added Git4Idea and GitHub plugins to the intellij block - Refactored CommandAutofixAction for more modular and extensible code diff --git a/build.gradle.kts b/build.gradle.kts index ba919578..32809448 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ version = properties("pluginVersion") val kotlin_version = "2.0.0-Beta5" // This line can be removed if not used elsewhere val jetty_version = "11.0.18" val slf4j_version = "2.0.9" -val skyenet_version = "1.0.86" +val skyenet_version = "1.0.91" val remoterobot_version = "0.11.21" val jackson_version = "2.17.0" @@ -40,7 +40,7 @@ dependencies { exclude(group = "org.jetbrains.kotlin", module = "") } - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.67") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.68") { exclude(group = "org.jetbrains.kotlin", module = "") } diff --git a/gradle.properties b/gradle.properties index 31def888..33b0e067 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.5.14 +pluginVersion=1.5.15 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/IndentedText.java b/src/main/java/com/github/simiacryptus/aicoder/util/IndentedText.java deleted file mode 100644 index 445d1db9..00000000 --- a/src/main/java/com/github/simiacryptus/aicoder/util/IndentedText.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.github.simiacryptus.aicoder.util; - -import com.simiacryptus.jopenai.util.StringUtil; - -import java.util.Arrays; -import java.util.stream.Collectors; - -/** - * This class provides a way to store and manipulate indented text blocks. - *

- * The text block is stored as a single string, with each line separated by a newline character. - * The indentation is stored as a separate string, which is prepended to each line when the text block is converted to a string. - *

- * The class provides a static method to convert a string to an IndentedText object. - * This method replaces all tab characters with two spaces, and then finds the minimum indentation of all lines. - * This indentation is then used as the indentation for the IndentedText object. - *

- * The class also provides a method to create a new IndentedText object with a different indentation. - */ -public class IndentedText implements TextBlock { - private CharSequence indent; - private CharSequence[] lines; - - public IndentedText(CharSequence indent, CharSequence... lines) { - this.setIndent(indent); - this.setLines(lines); - } - - /** - * This method is used to convert a string into an IndentedText object. - * - * @param text The string to be converted into an IndentedText object. - * @return IndentedText object created from the input string. - */ - public static IndentedText fromString(String text) { - text = text == null ? "" : text; - text = text.replace("\t", TextBlock.TAB_REPLACEMENT.toString()); - CharSequence indent = StringUtil.getWhitespacePrefix(text.split(TextBlock.DELIMITER)); - return new IndentedText(indent, Arrays.stream(text.split(TextBlock.DELIMITER)) - .map(s -> StringUtil.stripPrefix(s, indent)) - .toArray(CharSequence[]::new)); - } - - @Override - public String toString() { - return Arrays.stream(rawString()).collect(Collectors.joining(TextBlock.DELIMITER + getIndent())); - } - - @Override - public IndentedText withIndent(CharSequence indent) { - return new IndentedText(indent, getLines()); - } - - @Override - public CharSequence[] rawString() { - return this.getLines(); - } - - public CharSequence getIndent() { - return indent; - } - - public void setIndent(CharSequence indent) { - this.indent = indent; - } - - public CharSequence[] getLines() { - return lines; - } - - public void setLines(CharSequence[] lines) { - this.lines = lines; - } -} diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/TextBlock.java b/src/main/java/com/github/simiacryptus/aicoder/util/TextBlock.java deleted file mode 100644 index 34b4a076..00000000 --- a/src/main/java/com/github/simiacryptus/aicoder/util/TextBlock.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.simiacryptus.aicoder.util; - -import java.util.Arrays; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public interface TextBlock { - CharSequence TAB_REPLACEMENT = " "; - String DELIMITER = "\n"; - - CharSequence[] rawString(); - - default CharSequence getTextBlock() { - return Arrays.stream(rawString()).collect(Collectors.joining(DELIMITER)); - } - - TextBlock withIndent(CharSequence indent); - - default Stream stream() { - return Arrays.stream(rawString()); - } -} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/ApplyPatchAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/ApplyPatchAction.kt index a8324ee6..8b9b6cd3 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/ApplyPatchAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/ApplyPatchAction.kt @@ -1,6 +1,7 @@ package com.github.simiacryptus.aicoder.actions import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.ui.Messages @@ -12,6 +13,7 @@ class ApplyPatchAction : BaseAction( name = "Apply Patch", description = "Applies a patch to the current file" ) { + override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun handle(event: AnActionEvent) { val project = event.project ?: return diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt index 661c2eb9..0e6c7151 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt @@ -53,7 +53,7 @@ class DescribeAction : SelectionAction() { } return buildString { append(state.indent) - append(commentStyle?.fromString(wrapping)?.withIndent(state.indent) ?: wrapping) + append(commentStyle?.fromString(wrapping)?.withIndent(state.indent!!) ?: wrapping) append("\n") append(state.indent) append(state.selectedText) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt index 1292f7a9..6d594414 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt @@ -3,7 +3,6 @@ package com.github.simiacryptus.aicoder.actions.generic import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.FileSystemUtils.isGitignore import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent @@ -11,35 +10,14 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.vfs.VirtualFile -import com.simiacryptus.diff.FileValidationUtils -import com.simiacryptus.diff.FileValidationUtils.Companion.filteredWalk -import com.simiacryptus.diff.FileValidationUtils.Companion.isGitignore -import com.simiacryptus.diff.FileValidationUtils.Companion.isLLMIncludable -import com.simiacryptus.diff.addApplyFileDiffLinks -import com.simiacryptus.jopenai.describe.Description -import com.simiacryptus.jopenai.util.JsonUtil -import com.simiacryptus.skyenet.AgentPatterns -import com.simiacryptus.skyenet.Retryable -import com.simiacryptus.skyenet.core.actors.ParsedActor -import com.simiacryptus.skyenet.core.actors.SimpleActor -import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.apps.general.CmdPatchApp +import com.simiacryptus.skyenet.apps.general.PatchApp import com.simiacryptus.skyenet.core.platform.StorageInterface -import com.simiacryptus.skyenet.core.platform.User -import com.simiacryptus.skyenet.set -import com.simiacryptus.skyenet.webui.application.ApplicationInterface -import com.simiacryptus.skyenet.webui.application.ApplicationServer -import com.simiacryptus.skyenet.webui.application.ApplicationSocketManager -import com.simiacryptus.skyenet.webui.session.SessionTask -import com.simiacryptus.skyenet.webui.session.SocketManager -import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory import java.awt.BorderLayout import java.awt.Desktop import java.io.File import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.TimeUnit import javax.swing.* import kotlin.collections.set @@ -56,108 +34,10 @@ class CommandAutofixAction : BaseAction() { } else { event.project?.basePath?.let { File(it).toPath() } }!! - val session = StorageInterface.newGlobalID() - val patchApp = object : PatchApp(root.toFile(), session, settings) { - override fun codeFiles() = getFiles(virtualFiles) - .filter { it.toFile().length() < 1024 * 1024 / 2 } // Limit to 0.5MB - .map { root.relativize(it) ?: it }.toSet() - - override fun codeSummary(paths: List): String = paths - .filter { - val file = settings.workingDirectory?.resolve(it.toFile()) - file?.exists() == true && !file.isDirectory && file.length() < (256 * 1024) - } - .joinToString("\n\n") { path -> - try { - """ - |# ${path} - |$tripleTilde${path.toString().split('.').lastOrNull()} - |${settings.workingDirectory?.resolve(path.toFile())?.readText(Charsets.UTF_8)} - |$tripleTilde - """.trimMargin() - } catch (e: Exception) { - log.warn("Error reading file", e) - "Error reading file `${path}` - ${e.message}" - } - } - - override fun projectSummary(): String { - val codeFiles = codeFiles() - val str = codeFiles - .asSequence() - .filter { settings.workingDirectory?.toPath()?.resolve(it)?.toFile()?.exists() == true } - .distinct().sorted() - .joinToString("\n") { path -> - "* ${path} - ${ - settings.workingDirectory?.toPath()?.resolve(path)?.toFile()?.length() ?: "?" - } bytes".trim() - } - return str - } - - override fun output(task: SessionTask): OutputResult = run { - val command = listOf(settings.executable.absolutePath) + settings.arguments.split(" ") - val processBuilder = ProcessBuilder(command) - processBuilder.directory(settings.workingDirectory) - val buffer = StringBuilder() - val taskOutput = task.add("") - val process = processBuilder.start() - Thread { - var lastUpdate = 0L; - process.errorStream.bufferedReader().use { reader -> - var line: String? - while (reader.readLine().also { line = it } != null) { - buffer.append(line).append("\n") - if (lastUpdate + TimeUnit.SECONDS.toMillis(15) < System.currentTimeMillis()) { - taskOutput?.set("

\n${truncate(buffer.toString()).htmlEscape}\n
") - task.append("", true) - lastUpdate = System.currentTimeMillis() - } - } - task.append("", true) - } - }.start() - process.inputStream.bufferedReader().use { reader -> - var line: String? - var lastUpdate = 0L; - while (reader.readLine().also { line = it } != null) { - buffer.append(line).append("\n") - if (lastUpdate + TimeUnit.SECONDS.toMillis(15) < System.currentTimeMillis()) { - taskOutput?.set("
\n${outputString(buffer).htmlEscape}\n
") - task.append("", true) - lastUpdate = System.currentTimeMillis() - } - } - task.append("", true) - } - task.append("", false) - val exitCode = process.waitFor() - var output = outputString(buffer) - taskOutput?.clear() - OutputResult(exitCode, output) - } - - private fun outputString(buffer: StringBuilder): String { - var output = buffer.toString() - output = output.replace(Regex("\\x1B\\[[0-?]*[ -/]*[@-~]"), "") // Remove terminal escape codes - output = truncate(output) - return output - } - - override fun searchFiles(searchStrings: List): Set { - return searchStrings.flatMap { searchString -> - filteredWalk(settings.workingDirectory!!) { !isGitignore(it.toPath()) } - .filter { isLLMIncludable(it) } - .filter { it.readText().contains(searchString, ignoreCase = true) } - .map { it.toPath() } - .toList() - }.toSet() - } - } + val patchApp = CmdPatchApp(root, session, settings, api, virtualFiles?.map { it.toFile }?.toTypedArray(), AppSettingsState.instance.defaultSmartModel()) SessionProxyServer.chats[session] = patchApp val server = AppServer.getServer(event.project) - Thread { Thread.sleep(500) try { @@ -170,483 +50,133 @@ class CommandAutofixAction : BaseAction() { }.start() } - data class OutputResult(val exitCode: Int, val output: String) - abstract inner class PatchApp( - override val root: File, - val session: Session, - val settings: Settings, - ) : ApplicationServer( - applicationName = "Magic Code Fixer", - path = "/fixCmd", - showMenubar = false, - ) { - abstract fun codeFiles(): Set - abstract fun codeSummary(paths: List): String - abstract fun output(task: SessionTask): OutputResult - abstract fun searchFiles(searchStrings: List): Set - 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() - Retryable( - ui = ui, - task = task, - process = { content -> - val newTask = ui.newTask(false) - newTask.add("Running Command") - Thread { - run(ui, newTask, session, settings, task) - }.start() - newTask.placeholder - } - ) - return socketManager - } - - abstract fun projectSummary(): String - } - - private fun PatchApp.run( - ui: ApplicationInterface, - task: SessionTask, - session: Session, - settings: Settings, - mainTask: SessionTask - ) { - val output = output(task) - if (output.exitCode == 0 && settings.exitCodeOption == "nonzero") { - task.complete( - """ - |
- |
Command executed successfully
- |${renderMarkdown("${tripleTilde}\n${output.output}\n${tripleTilde}")} - |
- |""".trimMargin() - ) - return - } - if (settings.exitCodeOption == "zero" && output.exitCode != 0) { - task.complete( - """ - |
- |
Command failed
- |${renderMarkdown("${tripleTilde}\n${output.output}\n${tripleTilde}")} - |
- |""".trimMargin() - ) - return - } - try { - task.add( - """ - |
- |
Command exit code: ${output.exitCode}
- |${renderMarkdown("${tripleTilde}\n${output.output}\n${tripleTilde}")} - |
- """.trimMargin() - ) - fixAll(settings, output, task, ui, session, mainTask) - } catch (e: Exception) { - task.error(ui, e) - } - } + override fun isEnabled(event: AnActionEvent) = true - private fun PatchApp.fixAll( - settings: Settings, - output: OutputResult, - task: SessionTask, - ui: ApplicationInterface, - session: Session, - mainTask: SessionTask - ) { - Retryable(ui, task) { content -> - fixAllInternal(settings, output, task, ui, mainTask, mutableSetOf()) - content.clear() - "" - } - } + companion object { + private val log = LoggerFactory.getLogger(CommandAutofixAction::class.java) - private fun PatchApp.fixAllInternal( - settings: Settings, - output: OutputResult, - task: SessionTask, - ui: ApplicationInterface, - mainTask: SessionTask, - changed: MutableSet - ) { - val plan = ParsedActor( - resultClass = ParsedErrors::class.java, - prompt = """ - |You are a helpful AI that helps people with coding. - | - |You will be answering questions about the following project: - | - |Project Root: ${settings.workingDirectory?.absolutePath ?: ""} - | - |Files: - |${projectSummary()} - | - |Given the response of a build/test process, identify one or more distinct errors. - |For each error: - | 1) predict the files that need to be fixed - | 2) predict related files that may be needed to debug the issue - | 3) specify a search string to find relevant files - be as specific as possible - |${if (settings.additionalInstructions.isNotBlank()) "Additional Instructions:\n ${settings.additionalInstructions}\n" else ""} - """.trimMargin(), - model = AppSettingsState.instance.defaultSmartModel() - ).answer( - listOf( - """ - |The following command was run and produced an error: - | - |$tripleTilde - |${output.output} - |$tripleTilde - """.trimMargin() - ), api = api - ) - task.add( - AgentPatterns.displayMapInTabs( - mapOf( - "Text" to renderMarkdown(plan.text, ui = ui), - "JSON" to renderMarkdown( - "${tripleTilde}json\n${JsonUtil.toJson(plan.obj)}\n$tripleTilde", - ui = ui - ), - ) - ) - ) - val progressHeader = mainTask.header("Processing tasks") - plan.obj.errors?.forEach { error -> - task.header("Processing error: ${error.message}") - task.verbose(renderMarkdown("```json\n${JsonUtil.toJson(error)}\n```", tabs = false, ui = ui)) - // Search for files using the provided search strings - val searchResults = error.searchStrings?.flatMap { searchString -> - filteredWalk(settings.workingDirectory!!) { !isGitignore(it.toPath()) } - .filter { isLLMIncludable(it) } - .filter { it.readText().contains(searchString, ignoreCase = true) } - .map { it.toPath() } - .toList() - }?.toSet() ?: emptySet() - task.verbose( - renderMarkdown( - """ - |Search results: - | - |${searchResults.joinToString("\n") { "* `$it`" }} - """.trimMargin(), tabs = false, ui = ui - ) - ) - Retryable(ui, task) { content -> - fix( - error, searchResults.toList().map { it.toFile().absolutePath }, - output, ui, content, task, settings.autoFix, changed + private fun getUserSettings(event: AnActionEvent?): PatchApp.Settings? { + val root = UITools.getSelectedFolder(event ?: return null)?.toNioPath() ?: event.project?.basePath?.let { + File(it).toPath() + } + val files = UITools.getSelectedFiles(event).map { it.path.let { File(it).toPath() } }.toMutableSet() + if (files.isEmpty()) Files.walk(root) + .filter { Files.isRegularFile(it) && !Files.isDirectory(it) } + .toList().filterNotNull().forEach { files.add(it) } + val settingsUI = SettingsUI(root!!.toFile()) + val dialog = CommandSettingsDialog(event.project, settingsUI) + dialog.show() + return if (dialog.isOK) { + val executable = File(settingsUI.commandField.selectedItem?.toString() ?: return null) + AppSettingsState.instance.executables += executable.absolutePath + val argument = settingsUI.argumentsField.selectedItem?.toString() ?: "" + AppSettingsState.instance.recentArguments.remove(argument) + AppSettingsState.instance.recentArguments.add(0, argument) + AppSettingsState.instance.recentArguments = + AppSettingsState.instance.recentArguments.take(10).toMutableList() + PatchApp.Settings( + executable = executable, + arguments = argument, + workingDirectory = File(settingsUI.workingDirectoryField.text), + exitCodeOption = if (settingsUI.exitCodeZero.isSelected) "0" else if (settingsUI.exitCodeAny.isSelected) "any" else "nonzero", + additionalInstructions = settingsUI.additionalInstructionsField.text, + autoFix = settingsUI.autoFixCheckBox.isSelected ) - content.toString() + } else { + null } } - progressHeader?.clear() - mainTask.append("", false) - } - - private fun PatchApp.fix( - error: ParsedError, - additionalFiles: List? = null, - output: OutputResult, - ui: ApplicationInterface, - content: StringBuilder, - task: SessionTask, - autoFix: Boolean, - changed: MutableSet, - ) { - val paths = - ( - (error.fixFiles ?: emptyList()) + - (error.relatedFiles ?: emptyList()) + - (additionalFiles ?: emptyList()) - ).map { File(it).toPath() } - val prunedPaths = prunePaths(paths, 50 * 1024) - val summary = codeSummary(prunedPaths) - val response = SimpleActor( - prompt = """ - |You are a helpful AI that helps people with coding. - | - |You will be answering questions about the following code: - | - |$summary - | - | - |Response should use one or more code patches in diff format within ${tripleTilde}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 - |${tripleTilde}diff - | // Utility functions for example feature - | const b = 2; - | function exampleFunction() { - |- return b + 1; - |+ return b + 2; - | } - |$tripleTilde - | - |### tests/exampleUtils.test.js - |${tripleTilde}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); - | }); - | }); - |$tripleTilde - | - |If needed, new files can be created by using code blocks labeled with the filename in the same manner. - """.trimMargin(), - model = AppSettingsState.instance.defaultSmartModel() - ).answer( - listOf( - """ - |The following command was run and produced an error: - | - |${tripleTilde} - |${output.output} - |${tripleTilde} - | - |Focus on and Fix the Error: - | ${error.message?.replace("\n", "\n ") ?: ""} - |${if (settings.additionalInstructions.isNotBlank()) "Additional Instructions:\n ${settings.additionalInstructions}\n" else ""} - """.trimMargin() - ), api = api - ) - var markdown = ui.socketManager?.addApplyFileDiffLinks( - root = root.toPath(), - response = response, - ui = ui, - api = api, - shouldAutoApply = { path -> - if (autoFix && !changed.contains(path)) { - changed.add(path) - true - } else { - false + class SettingsUI(root: File) { + val argumentsField = ComboBox().apply { + isEditable = true + AppSettingsState.instance.recentArguments.forEach { addItem(it) } + if (AppSettingsState.instance.recentArguments.isEmpty()) { + addItem("run build") } } - ) - content.clear() - content.append("
${renderMarkdown(markdown!!)}
") - } - - private fun prunePaths(paths: List, maxSize: Int): List { - val sortedPaths = paths.sortedByDescending { it.toFile().length() } - var totalSize = 0 - val prunedPaths = mutableListOf() - for (path in sortedPaths) { - val fileSize = path.toFile().length().toInt() - if (totalSize + fileSize > maxSize) break - prunedPaths.add(path) - totalSize += fileSize - } - return prunedPaths - } - - data class ParsedErrors( - val errors: List? = null - ) - - data class ParsedError( - @Description("The error message") - val message: String? = null, - @Description("Files identified as needing modification and issue-related files") - val relatedFiles: List? = null, - @Description("Files identified as needing modification and issue-related files") - val fixFiles: List? = null, - @Description("Search strings to find relevant files") - val searchStrings: List? = null - ) - - - data class Settings( - var executable: File, - var arguments: String = "", - var workingDirectory: File? = null, - var exitCodeOption: String = "0", - var additionalInstructions: String = "", - val autoFix: Boolean, - ) - - private fun getFiles( - virtualFiles: Array? - ): MutableSet { - val codeFiles = mutableSetOf() // Set to avoid duplicates - virtualFiles?.forEach { file -> - if (file.isDirectory) { - if (file.name.startsWith(".")) return@forEach - if (isGitignore(file)) return@forEach - codeFiles.addAll(getFiles(file.children)) - } else { - codeFiles.add((file.toNioPath())) - } - } - return codeFiles - } - - private fun getUserSettings(event: AnActionEvent?): Settings? { - val root = UITools.getSelectedFolder(event ?: return null)?.toNioPath() ?: event.project?.basePath?.let { - File(it).toPath() - } - val files = UITools.getSelectedFiles(event).map { it.path.let { File(it).toPath() } }.toMutableSet() - if (files.isEmpty()) Files.walk(root) - .filter { Files.isRegularFile(it) && !Files.isDirectory(it) } - .toList().filterNotNull().forEach { files.add(it) } - val settingsUI = SettingsUI(root!!.toFile()) - val dialog = CommandSettingsDialog(event.project, settingsUI) - dialog.show() - return if (dialog.isOK) { - val executable = File(settingsUI.commandField.selectedItem?.toString() ?: return null) - AppSettingsState.instance.executables += executable.absolutePath - val argument = settingsUI.argumentsField.selectedItem?.toString() ?: "" - AppSettingsState.instance.recentArguments.remove(argument) - AppSettingsState.instance.recentArguments.add(0, argument) - AppSettingsState.instance.recentArguments = - AppSettingsState.instance.recentArguments.take(10).toMutableList() - Settings( - executable = executable, - arguments = argument, - workingDirectory = File(settingsUI.workingDirectoryField.text), - exitCodeOption = if (settingsUI.exitCodeZero.isSelected) "0" else if (settingsUI.exitCodeAny.isSelected) "any" else "nonzero", - additionalInstructions = settingsUI.additionalInstructionsField.text, - autoFix = settingsUI.autoFixCheckBox.isSelected - ) - } else { - null - } - } - - class SettingsUI(root: File) { - val argumentsField = ComboBox().apply { - isEditable = true - AppSettingsState.instance.recentArguments.forEach { addItem(it) } - if (AppSettingsState.instance.recentArguments.isEmpty()) { - addItem("run build") + val commandField = ComboBox(AppSettingsState.instance.executables.toTypedArray()).apply { + isEditable = true } - } - val commandField = ComboBox(AppSettingsState.instance.executables.toTypedArray()).apply { - isEditable = true - AppSettingsState.instance.executables.forEach { addItem(it) } - } - val commandButton = JButton("...").apply { - addActionListener { - val fileChooser = JFileChooser().apply { - fileSelectionMode = JFileChooser.FILES_ONLY - isMultiSelectionEnabled = false - } - if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { - commandField.selectedItem = fileChooser.selectedFile.absolutePath + val commandButton = JButton("...").apply { + addActionListener { + val fileChooser = JFileChooser().apply { + fileSelectionMode = JFileChooser.FILES_ONLY + isMultiSelectionEnabled = false + } + if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { + commandField.selectedItem = fileChooser.selectedFile.absolutePath + } } } - } - val workingDirectoryField = JTextField(root.absolutePath).apply { - isEditable = true - } - val workingDirectoryButton = JButton("...").apply { - addActionListener { - val fileChooser = JFileChooser().apply { - fileSelectionMode = JFileChooser.DIRECTORIES_ONLY - isMultiSelectionEnabled = false - this.selectedFile = File(workingDirectoryField.text) - } - if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { - workingDirectoryField.text = fileChooser.selectedFile.absolutePath + val workingDirectoryField = JTextField(root.absolutePath).apply { + isEditable = true + } + val workingDirectoryButton = JButton("...").apply { + addActionListener { + val fileChooser = JFileChooser().apply { + fileSelectionMode = JFileChooser.DIRECTORIES_ONLY + isMultiSelectionEnabled = false + this.selectedFile = File(workingDirectoryField.text) + } + if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { + workingDirectoryField.text = fileChooser.selectedFile.absolutePath + } } } - } - val exitCodeOptions = ButtonGroup() - val exitCodeNonZero = JRadioButton("Patch nonzero exit code", true) - val exitCodeZero = JRadioButton("Patch 0 exit code") - val exitCodeAny = JRadioButton("Patch any exit code") - val additionalInstructionsField = JTextArea().apply { - rows = 3 - lineWrap = true - wrapStyleWord = true - } - val autoFixCheckBox = JCheckBox("Auto-apply fixes").apply { - isSelected = false - } - } - - class CommandSettingsDialog(project: Project?, private val settingsUI: SettingsUI) : DialogWrapper(project) { - init { - settingsUI.exitCodeOptions.add(settingsUI.exitCodeNonZero) - settingsUI.exitCodeOptions.add(settingsUI.exitCodeZero) - settingsUI.exitCodeOptions.add(settingsUI.exitCodeAny) - title = "Command Autofix Settings" - init() + val exitCodeOptions = ButtonGroup() + val exitCodeNonZero = JRadioButton("Patch nonzero exit code", true) + val exitCodeZero = JRadioButton("Patch 0 exit code") + val exitCodeAny = JRadioButton("Patch any exit code") + val additionalInstructionsField = JTextArea().apply { + rows = 3 + lineWrap = true + wrapStyleWord = true + } + val autoFixCheckBox = JCheckBox("Auto-apply fixes").apply { + isSelected = false + } } - override fun createCenterPanel(): JComponent { - val panel = JPanel(BorderLayout()).apply { + class CommandSettingsDialog(project: Project?, private val settingsUI: SettingsUI) : DialogWrapper(project) { + init { + settingsUI.exitCodeOptions.add(settingsUI.exitCodeNonZero) + settingsUI.exitCodeOptions.add(settingsUI.exitCodeZero) + settingsUI.exitCodeOptions.add(settingsUI.exitCodeAny) + title = "Command Autofix Settings" + init() + } - val optionsPanel = JPanel().apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - add(JLabel("Executable")) - add(JPanel(BorderLayout()).apply { - add(settingsUI.commandField, BorderLayout.CENTER) - add(settingsUI.commandButton, BorderLayout.EAST) - }) - add(JLabel("Arguments")) - add(settingsUI.argumentsField) - add(JLabel("Working Directory")) - add(JPanel(BorderLayout()).apply { - add(settingsUI.workingDirectoryField, BorderLayout.CENTER) - add(settingsUI.workingDirectoryButton, BorderLayout.EAST) - }) - add(JLabel("Exit Code Options")) - add(settingsUI.exitCodeNonZero) - add(settingsUI.exitCodeAny) - add(settingsUI.exitCodeZero) - add(JLabel("Additional Instructions")) - add(JScrollPane(settingsUI.additionalInstructionsField)) - add(settingsUI.autoFixCheckBox) + override fun createCenterPanel(): JComponent { + val panel = JPanel(BorderLayout()).apply { + + val optionsPanel = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(JLabel("Executable")) + add(JPanel(BorderLayout()).apply { + add(settingsUI.commandField, BorderLayout.CENTER) + add(settingsUI.commandButton, BorderLayout.EAST) + }) + add(JLabel("Arguments")) + add(settingsUI.argumentsField) + add(JLabel("Working Directory")) + add(JPanel(BorderLayout()).apply { + add(settingsUI.workingDirectoryField, BorderLayout.CENTER) + add(settingsUI.workingDirectoryButton, BorderLayout.EAST) + }) + add(JLabel("Exit Code Options")) + add(settingsUI.exitCodeNonZero) + add(settingsUI.exitCodeAny) + add(settingsUI.exitCodeZero) + add(JLabel("Additional Instructions")) + add(JScrollPane(settingsUI.additionalInstructionsField)) + add(settingsUI.autoFixCheckBox) + } + add(optionsPanel, BorderLayout.SOUTH) } - add(optionsPanel, BorderLayout.SOUTH) + return panel } - return panel } - } - - override fun isEnabled(event: AnActionEvent) = true - - companion object { - private val log = LoggerFactory.getLogger(CommandAutofixAction::class.java) - const val tripleTilde = "`" + "``" // This is a workaround for the markdown parser when editing this file - - val String.htmlEscape: String - get() = this.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - fun truncate(output: String, kb: Int = 32): String { - var returnVal = output - if (returnVal.length > 1024 * 2 * kb) { - returnVal = returnVal.substring(0, 1024 * kb) + - "\n\n... Output truncated ...\n\n" + - returnVal.substring(returnVal.length - 1024 * kb) - } - return returnVal - } } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadAction.kt index d12e19d3..344d17f7 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadAction.kt @@ -4,54 +4,33 @@ import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.chatModel -import com.github.simiacryptus.aicoder.util.FileSystemUtils.expandFileList import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.PlatformDataKeys.VIRTUAL_FILE_ARRAY +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.isFile -import com.simiacryptus.diff.FileValidationUtils.Companion.isLLMIncludable -import com.simiacryptus.diff.addApplyFileDiffLinks -import com.simiacryptus.jopenai.API -import com.simiacryptus.jopenai.ApiModel -import com.simiacryptus.jopenai.ApiModel.Role +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.table.JBTable import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.util.ClientUtil.toContentList -import com.simiacryptus.jopenai.util.JsonUtil.toJson -import com.simiacryptus.skyenet.AgentPatterns.displayMapInTabs -import com.simiacryptus.skyenet.Discussable -import com.simiacryptus.skyenet.Retryable -import com.simiacryptus.skyenet.TabbedDisplay -import com.simiacryptus.skyenet.apps.coding.CodingAgent -import com.simiacryptus.skyenet.core.actors.* -import com.simiacryptus.skyenet.core.platform.ApplicationServices.clientManager -import com.simiacryptus.skyenet.core.platform.ClientManager -import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.apps.general.PlanAheadApp +import com.simiacryptus.skyenet.apps.plan.PlanCoordinator +import com.simiacryptus.skyenet.apps.plan.Settings import com.simiacryptus.skyenet.core.platform.StorageInterface -import com.simiacryptus.skyenet.core.platform.User import com.simiacryptus.skyenet.core.platform.file.DataStorage -import com.simiacryptus.skyenet.interpreter.ProcessInterpreter -import com.simiacryptus.skyenet.set -import com.simiacryptus.skyenet.webui.application.ApplicationInterface -import com.simiacryptus.skyenet.webui.application.ApplicationServer -import com.simiacryptus.skyenet.webui.session.SessionTask -import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory +import java.awt.BorderLayout import java.awt.Desktop +import java.awt.Dimension import java.awt.GridLayout -import java.io.File -import java.nio.file.Path -import java.util.* -import java.util.concurrent.Future -import java.util.concurrent.Semaphore -import java.util.concurrent.ThreadPoolExecutor -import java.util.concurrent.atomic.AtomicReference import javax.swing.* -import kotlin.reflect.KClass +import javax.swing.table.DefaultTableCellRenderer +import javax.swing.table.DefaultTableModel +import kotlin.collections.set + class PlanAheadAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -60,14 +39,27 @@ class PlanAheadAction : BaseAction() { var model: String = AppSettingsState.instance.smartModel, var temperature: Double = AppSettingsState.instance.temperature, var enableTaskPlanning: Boolean = false, - var enableShellCommands: Boolean = true, - var autoFix: Boolean = false + var enableShellCommands: Boolean = false, + var enableDocumentation: Boolean = false, + var enableFileModification: Boolean = true, + var enableInquiry: Boolean = true, + var enableCodeReview: Boolean = false, + var enableTestGeneration: Boolean = false, + var enableOptimization: Boolean = false, + var enableSecurityAudit: Boolean = false, + var enablePerformanceAnalysis: Boolean = false, + var enableRefactorTask: Boolean = false, + var enableForeachTask: Boolean = false, + var autoFix: Boolean = false, + var enableCommandAutoFix: Boolean = false, + var commandAutoFixCommands: List = listOf() ) class PlanAheadConfigDialog( project: Project?, private val settings: PlanAheadSettings ) : DialogWrapper(project) { + private val foreachTaskCheckbox = JCheckBox("Enable Foreach Task", settings.enableForeachTask) private val items = ChatModels.values().toList().toTypedArray() private val modelComboBox: ComboBox = ComboBox(items.map { it.first }.toTypedArray()) @@ -76,7 +68,50 @@ class PlanAheadAction : BaseAction() { private val taskPlanningCheckbox = JCheckBox("Enable Task Planning", settings.enableTaskPlanning) private val shellCommandsCheckbox = JCheckBox("Enable Shell Commands", settings.enableShellCommands) + private val documentationCheckbox = JCheckBox("Enable Documentation", settings.enableDocumentation) + private val fileModificationCheckbox = JCheckBox("Enable File Modification", settings.enableFileModification) + private val inquiryCheckbox = JCheckBox("Enable Inquiry", settings.enableInquiry) + private val codeReviewCheckbox = JCheckBox("Enable Code Review", settings.enableCodeReview) + private val testGenerationCheckbox = JCheckBox("Enable Test Generation", settings.enableTestGeneration) + private val optimizationCheckbox = JCheckBox("Enable Optimization", settings.enableOptimization) + private val securityAuditCheckbox = JCheckBox("Enable Security Audit", settings.enableSecurityAudit) + private val performanceAnalysisCheckbox = + JCheckBox("Enable Performance Analysis", settings.enablePerformanceAnalysis) + private val refactorTaskCheckbox = JCheckBox("Enable Refactor Task", settings.enableRefactorTask) private val autoFixCheckbox = JCheckBox("Auto-apply fixes", settings.autoFix) + private val checkboxStates = AppSettingsState.instance.executables.map { true }.toMutableList() + private val tableModel = object : DefaultTableModel(arrayOf("Enabled", "Command"), 0) { + + init { + AppSettingsState.instance.executables.forEach { command -> + addRow(arrayOf(true, command)) + } + } + + override fun getColumnClass(columnIndex: Int) = when (columnIndex) { + 0 -> java.lang.Boolean::class.java + else -> super.getColumnClass(columnIndex) + } + + override fun isCellEditable(row: Int, column: Int) = column == 0 + + override fun setValueAt(aValue: Any?, row: Int, column: Int) { + super.setValueAt(aValue, row, column) + if (column == 0 && aValue is Boolean) { + checkboxStates[row] = aValue + } else { + throw IllegalArgumentException("Invalid column index: $column") + } + } + + override fun getValueAt(row: Int, column: Int): Any = + if (column == 0) { + checkboxStates[row] + } else super.getValueAt(row, column) + } + private val commandTable = JBTable(tableModel).apply { putClientProperty("terminateEditOnFocusLost", true) } + private val addCommandButton = JButton("Add Command") + private val editCommandButton = JButton("Edit Command") init { init() @@ -85,10 +120,99 @@ class PlanAheadAction : BaseAction() { temperatureSlider.addChangeListener { settings.temperature = temperatureSlider.value / 100.0 } + val fileChooserDescriptor = FileChooserDescriptor(true, false, false, false, false, false) + .withTitle("Select Command") + .withDescription("Choose an executable file for the auto-fix command") + addCommandButton.addActionListener { + val chosenFile = FileChooser.chooseFile(fileChooserDescriptor, project, null) + if (chosenFile != null) { + val newCommand = chosenFile.path + val confirmResult = JOptionPane.showConfirmDialog( + null, + "Add command: $newCommand?", + "Confirm Command", + JOptionPane.YES_NO_OPTION + ) + if (confirmResult == JOptionPane.YES_OPTION) { + tableModel.addRow(arrayOf(false, newCommand)) + checkboxStates.add(true) + AppSettingsState.instance.executables.add(newCommand) + } + } + } + editCommandButton.addActionListener { + val selectedRow = commandTable.selectedRow + if (selectedRow != -1) { + val currentCommand = tableModel.getValueAt(selectedRow, 1) as String + val newCommand = JOptionPane.showInputDialog( + null, + "Edit command:", + currentCommand + ) + if (newCommand != null && newCommand.isNotEmpty()) { + val confirmResult = JOptionPane.showConfirmDialog( + null, + "Update command to: $newCommand?", + "Confirm Edit", + JOptionPane.YES_NO_OPTION + ) + if (confirmResult == JOptionPane.YES_OPTION) { + tableModel.setValueAt(newCommand, selectedRow, 1) + AppSettingsState.instance.executables.remove(currentCommand) + AppSettingsState.instance.executables.add(newCommand) + } + } + } else { + JOptionPane.showMessageDialog(null, "Please select a command to edit.") + } + } + commandTable.columnModel.getColumn(0).apply { + val checkBoxes = mutableMapOf() + fun jbCheckBox( + row: Int, + value: Any, + column: Int + ) = checkBoxes.getOrPut(row) { + JBCheckBox().apply { + this.isSelected = value as Boolean + this.addActionListener { + tableModel.setValueAt(this.isSelected, row, column) + } + } + } + + cellRenderer = object : DefaultTableCellRenderer() { + override fun getTableCellRendererComponent( + table: JTable, + value: Any, + isSelected: Boolean, + hasFocus: Boolean, + row: Int, + column: Int + ) = jbCheckBox(row, value, column) + } + cellEditor = object : DefaultCellEditor(JBCheckBox()) { + override fun getTableCellEditorComponent( + table: JTable, + value: Any, + isSelected: Boolean, + row: Int, + column: Int + ) = jbCheckBox(row, value, column) + } + preferredWidth = 60 + maxWidth = 60 + } + commandTable.selectionModel.addListSelectionListener { + editCommandButton.isEnabled = commandTable.selectedRow != -1 + } + editCommandButton.isEnabled = false } + override fun createCenterPanel(): JComponent { - val panel = JPanel(GridLayout(0, 2)) + val panel = JPanel() + panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS) panel.add(JLabel("Model:")) panel.add(modelComboBox) val indexOfFirst = items.indexOfFirst { @@ -99,10 +223,32 @@ class PlanAheadAction : BaseAction() { panel.add(temperatureSlider) panel.add(taskPlanningCheckbox) panel.add(shellCommandsCheckbox) + panel.add(documentationCheckbox) + panel.add(fileModificationCheckbox) + panel.add(inquiryCheckbox) + panel.add(codeReviewCheckbox) + panel.add(testGenerationCheckbox) + panel.add(optimizationCheckbox) + panel.add(securityAuditCheckbox) + panel.add(performanceAnalysisCheckbox) + panel.add(refactorTaskCheckbox) + panel.add(foreachTaskCheckbox) panel.add(autoFixCheckbox) + panel.add(JLabel("Auto-Fix Commands:")) + val scrollPane = JBScrollPane(commandTable) + scrollPane.preferredSize = Dimension(350, 100) + val tablePanel = JPanel(BorderLayout()) + tablePanel.add(scrollPane, BorderLayout.CENTER) + panel.add(tablePanel) + val buttonPanel = JPanel(GridLayout(1, 3)) + buttonPanel.add(addCommandButton) + panel.add(buttonPanel) + commandTable.isEnabled = true + addCommandButton.isEnabled = true return panel } + override fun doOKAction() { if (modelComboBox.selectedItem == null) { JOptionPane.showMessageDialog( @@ -116,11 +262,31 @@ class PlanAheadAction : BaseAction() { settings.model = modelComboBox.selectedItem as String settings.enableTaskPlanning = taskPlanningCheckbox.isSelected settings.enableShellCommands = shellCommandsCheckbox.isSelected + settings.enableDocumentation = documentationCheckbox.isSelected + settings.enableFileModification = fileModificationCheckbox.isSelected + settings.enableInquiry = inquiryCheckbox.isSelected + settings.enableCodeReview = codeReviewCheckbox.isSelected + settings.enableTestGeneration = testGenerationCheckbox.isSelected + settings.enableOptimization = optimizationCheckbox.isSelected + settings.enableSecurityAudit = securityAuditCheckbox.isSelected + settings.enablePerformanceAnalysis = performanceAnalysisCheckbox.isSelected + settings.enableRefactorTask = refactorTaskCheckbox.isSelected + settings.enableForeachTask = foreachTaskCheckbox.isSelected settings.autoFix = autoFixCheckbox.isSelected + settings.commandAutoFixCommands = (0 until tableModel.rowCount) + .filter { tableModel.getValueAt(it, 0) as Boolean } + .map { tableModel.getValueAt(it, 1) as String } + settings.enableCommandAutoFix = settings.commandAutoFixCommands.isNotEmpty() + // Update the global tool collection + AppSettingsState.instance.executables.clear() + AppSettingsState.instance.executables.addAll((0 until tableModel.rowCount).map { + tableModel.getValueAt(it, 1) as String + }) super.doOKAction() } } + val path = "/taskDev" override fun handle(e: AnActionEvent) { val project = e.project @@ -136,7 +302,36 @@ class PlanAheadAction : BaseAction() { ) DataStorage.sessionPaths[session] = root - SessionProxyServer.chats[session] = PlanAheadApp(event = e, root = root, settings = settings) + SessionProxyServer.chats[session] = PlanAheadApp( + rootFile = root, + settings = Settings( + documentationEnabled = settings.enableDocumentation, + fileModificationEnabled = settings.enableFileModification, + inquiryEnabled = settings.enableInquiry, + codeReviewEnabled = settings.enableCodeReview, + testGenerationEnabled = settings.enableTestGeneration, + optimizationEnabled = settings.enableOptimization, + securityAuditEnabled = settings.enableSecurityAudit, + performanceAnalysisEnabled = settings.enablePerformanceAnalysis, + refactorTaskEnabled = settings.enableRefactorTask, + foreachTaskEnabled = settings.enableForeachTask, + model = settings.model.chatModel(), // Use the model from settings + temperature = settings.temperature, // Use the temperature from settings + taskPlanningEnabled = settings.enableTaskPlanning, // Use the task planning flag from settings + shellCommandTaskEnabled = settings.enableShellCommands, // Use the shell command flag from settings + autoFix = settings.autoFix, // Use the autoFix flag from settings + enableCommandAutoFix = settings.enableCommandAutoFix, // Use the enableCommandAutoFix flag from settings + commandAutoFixCommands = settings.commandAutoFixCommands, // Use the commandAutoFixCommands from settings + env = mapOf(), + workingDir = root.absolutePath, + language = if (PlanCoordinator.isWindows) "powershell" else "bash", + command = listOf(if (System.getProperty("os.name").lowercase().contains("win")) "powershell" else "bash"), + parsingModel = AppSettingsState.instance.defaultFastModel(), + ), + model = AppSettingsState.instance.defaultSmartModel(), + parsingModel = AppSettingsState.instance.defaultFastModel(), + showMenubar = false + ) val server = AppServer.getServer(project) openBrowser(server, session.toString()) @@ -147,7 +342,6 @@ class PlanAheadAction : BaseAction() { Thread { Thread.sleep(500) try { - val uri = server.server.uri.resolve("/#$session") log.info("Opening browser to $uri") Desktop.getDesktop().browse(uri) @@ -157,1185 +351,8 @@ class PlanAheadAction : BaseAction() { }.start() } - companion object { - private val log = LoggerFactory.getLogger(PlanAheadAction::class.java) - - } -} - -class PlanAheadApp( - applicationName: String = "Task Planning v1.1", - path: String = "/taskDev", - val event: AnActionEvent, - override val root: File, - val settings: PlanAheadAction.PlanAheadSettings, -) : ApplicationServer( - applicationName = applicationName, - path = path, - showMenubar = false, -) { - data class Settings( - val model: ChatModels = AppSettingsState.instance.smartModel.chatModel(), - val parsingModel: ChatModels = AppSettingsState.instance.fastModel.chatModel(), - val temperature: Double = 0.2, - val budget: Double = 2.0, - val taskPlanningEnabled: Boolean = false, - val shellCommandTaskEnabled: Boolean = true, - val autoFix: Boolean = false, - ) - - override val settingsClass: Class<*> get() = Settings::class.java - - @Suppress("UNCHECKED_CAST") - override fun initSettings(session: Session): T = Settings( - model = ChatModels.values().filter { settings.model == it.key || settings.model == it.value.name } - .map { it.value }.first(), // Use the model from settings - temperature = settings.temperature, // Use the temperature from settings - taskPlanningEnabled = settings.enableTaskPlanning, // Use the task planning flag from settings - shellCommandTaskEnabled = settings.enableShellCommands, // Use the shell command flag from settings - autoFix = settings.autoFix // Use the autoFix flag from settings - ) as T - - override fun userMessage( - session: Session, - user: User?, - userMessage: String, - ui: ApplicationInterface, - api: API - ) { - try { - val settings = getSettings(session, user) - if (api is ClientManager.MonitoredClient) api.budget = settings?.budget ?: 2.0 - PlanAheadAgent( - user = user, - session = session, - dataStorage = dataStorage, - api = api, - ui = ui, - model = settings?.model ?: AppSettingsState.instance.smartModel.chatModel(), - parsingModel = settings?.parsingModel ?: AppSettingsState.instance.fastModel.chatModel(), - temperature = settings?.temperature ?: 0.3, - event = event, - workingDir = root.absolutePath, - root = root.toPath(), - taskPlanningEnabled = settings?.taskPlanningEnabled ?: false, - shellCommandTaskEnabled = settings?.shellCommandTaskEnabled ?: true, - autoFix = settings?.autoFix ?: false, - ).startProcess(userMessage = userMessage) - } catch (e: Throwable) { - ui.newTask().error(ui, e) - log.warn("Error", e) - } - } companion object { - private val log = LoggerFactory.getLogger(PlanAheadApp::class.java) - } -} - -private const val tripleTilde = "```" - -class PlanAheadAgent( - user: User?, - session: Session, - dataStorage: StorageInterface, - val ui: ApplicationInterface, - val api: API, - model: ChatModels = ChatModels.GPT4o, - parsingModel: ChatModels = ChatModels.GPT35Turbo, - temperature: Double = 0.3, - val taskPlanningEnabled: Boolean, - val shellCommandTaskEnabled: Boolean, - private val autoFix: Boolean, - private val env: Map = mapOf(), - val workingDir: String = ".", - val language: String = if (isWindows) "powershell" else "bash", - private val command: List = listOf(AppSettingsState.instance.shellCommand), - private val actorMap: Map> = mapOf( - ActorTypes.TaskBreakdown to planningActor( - taskPlanningEnabled, - shellCommandTaskEnabled, - model, - parsingModel, - temperature - ), - ActorTypes.DocumentationGenerator to documentActor(model, temperature), - ActorTypes.NewFileCreator to createFileActor(model, temperature), - ActorTypes.FilePatcher to patchActor(model, temperature), - ActorTypes.Inquiry to inquiryActor( - taskPlanningEnabled, - shellCommandTaskEnabled, - model, - temperature - ), - ) + (if (!shellCommandTaskEnabled) mapOf() else mapOf( - ActorTypes.RunShellCommand to shellActor(env, workingDir, language, command, model, temperature), - )), - val event: AnActionEvent, - val root: Path -) : ActorSystem( - actorMap.map { it.key.name to it.value }.toMap(), - dataStorage, - user, - session -) { - private val documentationGeneratorActor by lazy { actorMap[ActorTypes.DocumentationGenerator] as SimpleActor } - private val taskBreakdownActor by lazy { actorMap[ActorTypes.TaskBreakdown] as ParsedActor } - private val newFileCreatorActor by lazy { actorMap[ActorTypes.NewFileCreator] as SimpleActor } - private val filePatcherActor by lazy { actorMap[ActorTypes.FilePatcher] as SimpleActor } - private val inquiryActor by lazy { actorMap[ActorTypes.Inquiry] as SimpleActor } - val shellCommandActor by lazy { actorMap[ActorTypes.RunShellCommand] as CodingActor } - - data class TaskBreakdownResult( - val tasksByID: Map? = null, - val finalTaskID: String? = null, - ) - - data class Task( - val description: String? = null, - val taskType: TaskType? = null, - var task_dependencies: List? = null, - val input_files: List? = null, - val output_files: List? = null, - var state: TaskState? = null, - ) - - enum class TaskState { - Pending, - InProgress, - Completed, - } - - enum class TaskType { - TaskPlanning, - Inquiry, - NewFile, - EditFile, - Documentation, - RunShellCommand, - } - - private val virtualFiles by lazy { - expandFileList(VIRTUAL_FILE_ARRAY.getData(event.dataContext) ?: arrayOf()) - } - - private val codeFiles - get() = virtualFiles - .filter { it.exists() && it.isFile } - .filter { !it.name.startsWith(".") } - .associate { file -> getKey(file) to getValue(file) } - - - private fun getValue(file: VirtualFile) = try { - file.inputStream.bufferedReader().use { it.readText() } - } catch (e: Exception) { - log.warn("Error reading file", e) - "" - } - - private fun getKey(file: VirtualFile) = root.relativize(file.toNioPath()) - - fun startProcess(userMessage: String) { - val codeFiles = codeFiles - val eventStatus = if (!codeFiles.all { it.key.toFile().isFile } || codeFiles.size > 2) """ - Files: - ${codeFiles.keys.joinToString("\n") { "* ${it}" }} - """.trimMargin() else { - """ - |${ - virtualFiles.joinToString("\n\n") { - val path = root.relativize(it.toNioPath()) - """ - ## $path - | - ${(codeFiles[path] ?: "").let { "$tripleTilde\n${it/*.indent(" ")*/}\n$tripleTilde" }} - """.trimMargin() - } - } - """.trimMargin() - } - val task = ui.newTask() - val toInput = { it: String -> - listOf( - eventStatus, - it - ) - } - val highLevelPlan = Discussable( - task = task, - heading = renderMarkdown(userMessage, ui = ui), - userMessage = { userMessage }, - initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, - outputFn = { design: ParsedResponse -> - displayMapInTabs( - mapOf( - "Text" to renderMarkdown(design.text, ui = ui), - "JSON" to renderMarkdown( - "${tripleTilde}json\n${toJson(design.obj)/*.indent(" ")*/}\n$tripleTilde", - ui = ui - ), - ) - ) - }, - ui = ui, - reviseResponse = { userMessages: List> -> - taskBreakdownActor.respond( - messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } - .toTypedArray()), - input = toInput(userMessage), - api = api - ) - }, - ).call() - - initPlan(highLevelPlan, userMessage, task) - } - - private fun initPlan( - plan: ParsedResponse, - userMessage: String, - task: SessionTask - ) { - try { - val tasksByID = - plan.obj.tasksByID?.entries?.toTypedArray()?.associate { it.key to it.value } ?: mapOf() - val pool: ThreadPoolExecutor = clientManager.getPool(session, user) - val genState = GenState(tasksByID.toMutableMap()) - val diagramTask = ui.newTask(false).apply { task.add(placeholder) } - val diagramBuffer = - diagramTask.add( - renderMarkdown( - "## Task Dependency Graph\n${tripleTilde}mermaid\n${buildMermaidGraph(genState.subTasks)}\n$tripleTilde", - ui = ui - ) - ) - val taskTabs = object : TabbedDisplay(ui.newTask(false).apply { task.add(placeholder) }) { - override fun renderTabButtons(): String { - diagramBuffer?.set( - renderMarkdown( - "## Task Dependency Graph\n${tripleTilde}mermaid\n${ - buildMermaidGraph( - genState.subTasks - ) - }\n$tripleTilde", ui = ui - ) - ) - diagramTask.complete() - return buildString { - append("
\n") - super.tabs.withIndex().forEach { (idx, t) -> - val (taskId, taskV) = t - val subTask = genState.tasksByDescription[taskId] - if (null == subTask) { - log.warn("Task tab not found: $taskId") - } - val isChecked = if (taskId in genState.taskIdProcessingQueue) "checked" else "" - val style = when (subTask?.state) { - TaskState.Completed -> " style='text-decoration: line-through;'" - null -> " style='opacity: 20%;'" - TaskState.Pending -> " style='opacity: 30%;'" - else -> "" - } - append("
\n") - } - append("
") - } - } - } - genState.taskIdProcessingQueue.forEach { taskId -> - val newTask = ui.newTask(false) - genState.uitaskMap[taskId] = newTask - val subtask = genState.subTasks[taskId] - val description = subtask?.description - log.debug("Creating task tab: $taskId ${System.identityHashCode(subtask)} $description") - taskTabs[description ?: taskId] = newTask.placeholder - } - Thread.sleep(100) - while (genState.taskIdProcessingQueue.isNotEmpty()) { - val taskId = genState.taskIdProcessingQueue.removeAt(0) - val subTask = genState.subTasks[taskId] ?: throw RuntimeException("Task not found: $taskId") - genState.taskFutures[taskId] = pool.submit { - subTask.state = TaskState.Pending - taskTabs.update() - log.debug("Awaiting dependencies: ${subTask.task_dependencies?.joinToString(", ") ?: ""}") - subTask.task_dependencies - ?.associate { it to genState.taskFutures[it] } - ?.forEach { (id, future) -> - try { - future?.get() ?: log.warn("Dependency not found: $id") - } catch (e: Throwable) { - log.warn("Error", e) - } - } - subTask.state = TaskState.InProgress - taskTabs.update() - log.debug("Running task: ${System.identityHashCode(subTask)} ${subTask.description}") - runTask( - taskId = taskId, - subTask = subTask, - userMessage = userMessage, - plan = plan, - genState = genState, - task = genState.uitaskMap.get(taskId) ?: ui.newTask(false).apply { - taskTabs[taskId] = placeholder - }, - taskTabs = taskTabs - ) - } - } - genState.taskFutures.forEach { (id, future) -> - try { - future.get() ?: log.warn("Dependency not found: $id") - } catch (e: Throwable) { - log.warn("Error", e) - } - } - } catch (e: Throwable) { - log.warn("Error during incremental code generation process", e) - task.error(ui, e) - } - } - - data class GenState( - val subTasks: Map, - val tasksByDescription: MutableMap = subTasks.entries.toTypedArray() - .associate { it.value.description to it.value }.toMutableMap(), - val taskIdProcessingQueue: MutableList = executionOrder(subTasks).toMutableList(), - val taskResult: MutableMap = mutableMapOf(), - val completedTasks: MutableList = mutableListOf(), - val taskFutures: MutableMap> = mutableMapOf(), - val uitaskMap: MutableMap = mutableMapOf(), - ) - - private fun runTask( - taskId: String, - subTask: Task, - userMessage: String, - plan: ParsedResponse, - genState: GenState, - task: SessionTask, - taskTabs: TabbedDisplay, - ) { - try { - val dependencies = subTask.task_dependencies?.toMutableSet() ?: mutableSetOf() - dependencies += getAllDependencies(subTask, genState.subTasks) - val priorCode = dependencies - .joinToString("\n\n\n") { dependency -> - """ - |# $dependency - | - |${genState.taskResult[dependency] ?: ""} - """.trimMargin() - } - val codeFiles = codeFiles - fun inputFileCode() = ((subTask.input_files ?: listOf()) + (subTask.output_files ?: listOf())) - .filter { isLLMIncludable(root.toFile().resolve(it)) }.joinToString("\n\n") { - try { - """ - |# $it - | - |$tripleTilde - |${codeFiles[File(it).toPath()] ?: root.resolve(it).toFile().readText()} - |$tripleTilde - """.trimMargin() - } catch (e: Throwable) { - log.warn("Error: root=$root ", e) - "" - } - } - task.add( - renderMarkdown( - """ - |## Task `${taskId}` - |${subTask.description ?: ""} - | - |${tripleTilde}json - |${toJson(subTask)/*.indent(" ")*/} - |$tripleTilde - | - |### Dependencies: - |${dependencies.joinToString("\n") { "- $it" }} - | - """.trimMargin(), ui = ui - ) - ) - - when (subTask.taskType) { - - TaskType.NewFile -> { - val semaphore = Semaphore(0) - createFiles( - task = task, - userMessage = userMessage, - highLevelPlan = plan, - priorCode = priorCode, - inputFileCode = ::inputFileCode, - subTask = subTask, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { semaphore.release() } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) - } - - } - - TaskType.EditFile -> { - val semaphore = Semaphore(0) - editFiles( - task = task, - userMessage = userMessage, - highLevelPlan = plan, - priorCode = priorCode, - inputFileCode = ::inputFileCode, - subTask = subTask, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { semaphore.release() } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) - } - } - - TaskType.Documentation -> { - val semaphore = Semaphore(0) - document( - task = task, - userMessage = userMessage, - highLevelPlan = plan, - priorCode = priorCode, - inputFileCode = ::inputFileCode, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { - semaphore.release() - } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) - } - } - - TaskType.Inquiry -> { - inquiry( - subTask = subTask, - userMessage = userMessage, - highLevelPlan = plan, - priorCode = priorCode, - inputFileCode = ::inputFileCode, - genState = genState, - taskId = taskId, - task = task, - taskTabs = taskTabs, - ) - } - - TaskType.TaskPlanning -> { - if (!taskPlanningEnabled) throw RuntimeException("Task planning is disabled") - taskPlanning( - subTask = subTask, - userMessage = userMessage, - highLevelPlan = plan, - priorCode = priorCode, - inputFileCode = ::inputFileCode, - genState = genState, - taskId = taskId, - task = task, - taskTabs = taskTabs, - ) - } - - TaskType.RunShellCommand -> { - if (shellCommandTaskEnabled) { - val semaphore = Semaphore(0) - runShellCommand( - task = task, - userMessage = userMessage, - highLevelPlan = plan, - priorCode = priorCode, - inputFileCode = ::inputFileCode, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { - semaphore.release() - } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) - } - log.debug("Completed shell command: $taskId") - } - } - - else -> null - } - } catch (e: Exception) { - log.warn("Error during task execution", e) - task.error(ui, e) - } finally { - genState.completedTasks.add(taskId) - subTask.state = TaskState.Completed - log.debug("Completed task: $taskId ${System.identityHashCode(subTask)}") - taskTabs.update() - } - } - - private fun runShellCommand( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: () -> String, - genState: GenState, - taskId: String, - taskTabs: TabbedDisplay, - function: () -> Unit - ) { - object : CodingAgent( - api = api, - dataStorage = dataStorage, - session = session, - user = user, - ui = ui, - interpreter = shellCommandActor.interpreterClass as KClass, - symbols = shellCommandActor.symbols, - temperature = shellCommandActor.temperature, - details = shellCommandActor.details, - model = shellCommandActor.model, - mainTask = task, - ) { - override fun displayFeedback( - task: SessionTask, - request: CodingActor.CodeRequest, - response: CodingActor.CodeResult - ) { - val formText = StringBuilder() - var formHandle: StringBuilder? = null - formHandle = task.add( - """ - |
- |${if (!super.canPlay) "" else super.playButton(task, request, response, formText) { formHandle!! }} - |${acceptButton(task, request, response, formText) { formHandle!! }} - |
- |${super.reviseMsg(task, request, response, formText) { formHandle!! }} - """.trimMargin(), className = "reply-message" - ) - formText.append(formHandle.toString()) - formHandle.toString() - task.complete() - } - - fun acceptButton( - task: SessionTask, - request: CodingActor.CodeRequest, - response: CodingActor.CodeResult, - formText: StringBuilder, - formHandle: () -> StringBuilder - ): String { - return ui.hrefLink("Accept", "href-link play-button") { - genState.taskResult[taskId] = response.let { - """ - |## Shell Command Output - | - |$tripleTilde - |${response.code} - |$tripleTilde - | - |$tripleTilde - |${response.renderedResponse} - |$tripleTilde - """.trimMargin() - } - function() - } - } - }.apply { - start( - codeRequest( - listOf( - userMessage to Role.user, - highLevelPlan.text to Role.assistant, - priorCode to Role.assistant, - inputFileCode() to Role.assistant, - ) - ) - ) - } - } - - private fun createFiles( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: () -> String, - subTask: Task, - genState: GenState, - taskId: String, - taskTabs: TabbedDisplay, - onComplete: () -> Unit - ) { - - val process = { sb: StringBuilder -> - val codeResult = newFileCreatorActor.answer( - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode(), - subTask.description ?: "", - ).filter { it.isNotBlank() }, api - ) - genState.taskResult[taskId] = codeResult - if (autoFix) { - val diffLinks = ui.socketManager!!.addApplyFileDiffLinks( - root, - codeResult, - api = api, - ui = ui, - shouldAutoApply = { true }) - taskTabs.selectedTab += 1 - taskTabs.update() - onComplete() - renderMarkdown(diffLinks + "\n\n## Auto-applied changes", ui = ui) - } else { - renderMarkdown( - ui.socketManager!!.addApplyFileDiffLinks(root, codeResult, api = api, ui = ui), - ui = ui - ) + acceptButtonFooter(sb) { - taskTabs.selectedTab += 1 - taskTabs.update() - onComplete() - } - } - } - Retryable(ui, task, process) - } - - private fun editFiles( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: () -> String, - subTask: Task, - genState: GenState, - taskId: String, - taskTabs: TabbedDisplay, - onComplete: () -> Unit, - ) { - val process = { sb: StringBuilder -> - val codeResult = filePatcherActor.answer( - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode(), - subTask.description ?: "", - ).filter { it.isNotBlank() }, api - ) - genState.taskResult[taskId] = codeResult - if (autoFix) { - val diffLinks = ui.socketManager!!.addApplyFileDiffLinks( - root = root, - response = codeResult, - handle = { newCodeMap -> - newCodeMap.forEach { (path, newCode) -> - task.complete("$path Updated") - } - }, - ui = ui, - api = api, - shouldAutoApply = { true } - ) - taskTabs.selectedTab += 1 - taskTabs.update() - task.complete() - onComplete() - renderMarkdown(diffLinks + "\n\n## Auto-applied changes", ui = ui) - } else { - renderMarkdown( - ui.socketManager!!.addApplyFileDiffLinks( - root = root, - response = codeResult, - handle = { newCodeMap -> - newCodeMap.forEach { (path, newCode) -> - task.complete("$path Updated") - } - }, - ui = ui, - api = api - ) + acceptButtonFooter(sb) { - taskTabs.selectedTab += 1 - taskTabs.update() - task.complete() - onComplete() - }, ui = ui - ) - } - } - Retryable(ui, task, process) - } - - private fun document( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: () -> String, - genState: GenState, - taskId: String, - taskTabs: TabbedDisplay, - onComplete: () -> Unit - ) { - val process = { sb: StringBuilder -> - val docResult = documentationGeneratorActor.answer( - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode(), - ).filter { it.isNotBlank() }, api - ) - genState.taskResult[taskId] = docResult - if (autoFix) { - taskTabs.selectedTab += 1 - taskTabs.update() - task.complete() - onComplete() - renderMarkdown("## Generated Documentation\n$docResult\nAuto-accepted", ui = ui) - } else { - renderMarkdown("## Generated Documentation\n$docResult", ui = ui) + acceptButtonFooter(sb) { - taskTabs.selectedTab += 1 - taskTabs.update() - task.complete() - onComplete() - } - } - } - Retryable(ui, task, process) - } - - private fun acceptButtonFooter(stringBuilder: StringBuilder, fn: () -> Unit): String { - val footerTask = ui.newTask(false) - lateinit var textHandle: StringBuilder - textHandle = footerTask.complete(ui.hrefLink("Accept", classname = "href-link cmd-button") { - try { - textHandle.set("""
Accepted
""") - footerTask.complete() - } catch (e: Throwable) { - log.warn("Error", e) - } - fn() - })!! - return footerTask.placeholder - } - - private fun inquiry( - subTask: Task, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: () -> String, - genState: GenState, - taskId: String, - task: SessionTask, - taskTabs: TabbedDisplay - ) { - val input1 = "Expand ${subTask.description ?: ""}" - val toInput = { it: String -> - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode(), - it, - ).filter { it.isNotBlank() } - } - val inquiryResult = Discussable( - task = task, - userMessage = { "Expand ${subTask.description ?: ""}\n${toJson(subTask)}" }, - heading = "", - initialResponse = { it: String -> inquiryActor.answer(toInput(it), api = api) }, - outputFn = { design: String -> - renderMarkdown(design, ui = ui) - }, - ui = ui, - reviseResponse = { userMessages: List> -> - inquiryActor.respond( - messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } - .toTypedArray()), - input = toInput("Expand ${subTask.description ?: ""}\n${toJson(subTask)}"), - api = api - ) - }, - atomicRef = AtomicReference(), - semaphore = Semaphore(0), - ).call() - genState.taskResult[taskId] = inquiryResult - } - - private fun taskPlanning( - subTask: Task, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: () -> String, - genState: GenState, - taskId: String, - task: SessionTask, - taskTabs: TabbedDisplay - ) { - val toInput = { it: String -> - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode(), - it - ).filter { it.isNotBlank() } - } - val input1 = "Expand ${subTask.description ?: ""}\n${toJson(subTask)}" - val subPlan: ParsedResponse = Discussable( - task = task, - userMessage = { input1 }, - heading = "", - initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, - outputFn = { design: ParsedResponse -> - displayMapInTabs( - mapOf( - "Text" to renderMarkdown(design.text, ui = ui), - "JSON" to renderMarkdown( - "${tripleTilde}json\n${toJson(design.obj)/*.indent(" ")*/}\n$tripleTilde", - ui = ui - ), - ) - ) - }, - ui = ui, - reviseResponse = { userMessages: List> -> - taskBreakdownActor.respond( - messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } - .toTypedArray()), - input = toInput(input1), - api = api - ) - }, - ).call() - initPlan( - plan = subPlan, - userMessage = userMessage, - task = task, - ) - } - - private fun getAllDependencies(subTask: Task, subTasks: Map): List { - return getAllDependenciesHelper(subTask, subTasks, mutableSetOf()) - } - - private fun getAllDependenciesHelper( - subTask: Task, - subTasks: Map, - visited: MutableSet - ): List { - val dependencies = subTask.task_dependencies?.toMutableList() ?: mutableListOf() - subTask.task_dependencies?.forEach { dep -> - if (dep in visited) return@forEach - val subTask = subTasks[dep] - if (subTask != null) { - visited.add(dep) - dependencies.addAll(getAllDependenciesHelper(subTask, subTasks, visited)) - } - } - return dependencies - } - - private fun buildMermaidGraph(subTasks: Map): String { - val graphBuilder = StringBuilder("graph TD;\n") - subTasks.forEach { (taskId, task) -> - val sanitizedTaskId = sanitizeForMermaid(taskId) - val taskType = task.taskType?.name ?: "Unknown" - val escapedDescription = escapeMermaidCharacters(task.description ?: "") - graphBuilder.append(" ${sanitizedTaskId}[$escapedDescription]:::$taskType;\n") - task.task_dependencies?.forEach { dependency -> - val sanitizedDependency = sanitizeForMermaid(dependency) - graphBuilder.append(" ${sanitizedDependency} --> ${sanitizedTaskId};\n") - } - } - graphBuilder.append(" classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef NewFile fill:lightblue,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef EditFile fill:lightgreen,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef Documentation fill:lightyellow,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef Inquiry fill:orange,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef TaskPlanning fill:lightgrey,stroke:#333,stroke-width:2px;\n") - return graphBuilder.toString() - } - - private fun sanitizeForMermaid(input: String) = input - .replace(" ", "_") - .replace("\"", "\\\"") - .replace("[", "\\[") - .replace("]", "\\]") - .replace("(", "\\(") - .replace(")", "\\)") - .let { "`$it`" } - - private fun escapeMermaidCharacters(input: String) = input - .replace("\"", "\\\"") - .let { '"' + it + '"' } - - companion object { - private val log = LoggerFactory.getLogger(PlanAheadAgent::class.java) - - enum class ActorTypes { - TaskBreakdown, - DocumentationGenerator, - NewFileCreator, - FilePatcher, - Inquiry, - RunShellCommand, - } - - fun executionOrder(tasks: Map): List { - val taskIds: MutableList = mutableListOf() - val taskMap = tasks.toMutableMap() - while (taskMap.isNotEmpty()) { - val nextTasks = - taskMap.filter { (_, task) -> task.task_dependencies?.all { taskIds.contains(it) } ?: true } - if (nextTasks.isEmpty()) { - throw RuntimeException("Circular dependency detected in task breakdown") - } - taskIds.addAll(nextTasks.keys) - nextTasks.keys.forEach { taskMap.remove(it) } - } - return taskIds - } - - val isWindows = System.getProperty("os.name").lowercase(Locale.getDefault()).contains("windows") - + private val log = LoggerFactory.getLogger(PlanAheadAction::class.java) } -} - -private fun documentActor( - model: ChatModels, - temperature: Double -) = SimpleActor( - name = "DocumentationGenerator", - prompt = """ - Create detailed and clear documentation for the provided code, covering its purpose, functionality, inputs, outputs, and any assumptions or limitations. - Use a structured and consistent format that facilitates easy understanding and navigation. - Include code examples where applicable, and explain the rationale behind key design decisions and algorithm choices. - Document any known issues or areas for improvement, providing guidance for future developers on how to extend or maintain the code. - """.trimIndent(), - model = model, - temperature = temperature, -) - -private fun planningActor( - taskPlanningEnabled: Boolean, - shellCommandTaskEnabled: Boolean, - model: ChatModels, - parsingModel: ChatModels, - temperature: Double -): ParsedActor = - ParsedActor( - name = "TaskBreakdown", - resultClass = PlanAheadAgent.TaskBreakdownResult::class.java, - prompt = """ - |Given a user request, identify and list smaller, actionable tasks that can be directly implemented in code. - |Detail files input and output as well as task execution dependencies. - |Creating directories and initializing source control are out of scope. - | - |Tasks can be of the following types: - | - |* Inquiry - Answer questions by reading in files and providing a summary that can be discussed with and approved by the user - | ** Specify the questions and the goal of the inquiry - | ** List input files to be examined when answering the questions - |* NewFile - Create one or more new files, carefully considering how they fit into the existing project structure - | ** For each file, specify the relative file path and the purpose of the file - | ** List input files/tasks to be examined when authoring the new files - |* EditFile - Modify existing files - | ** For each file, specify the relative file path and the goal of the modification - | ** List input files/tasks to be examined when designing the modifications - |* Documentation - Generate documentation - | ** List input files/tasks to be examined - |${ - if (!shellCommandTaskEnabled) "" else """ - |* RunShellCommand - Execute shell commands and provide the output - | ** Specify the command to be executed, or describe the task to be performed - | ** List input files/tasks to be examined when writing the command - """.trimMargin().trim() - } - |${ - if (!taskPlanningEnabled) "" else """ - |* TaskPlanning - High-level planning and organization of tasks - identify smaller, actionable tasks based on the information available at task execution time. - | ** Specify the prior tasks and the goal of the task - """.trimMargin().trim() - } - """.trimMargin(), - model = model, - parsingModel = parsingModel, - temperature = temperature, - ) - -private fun createFileActor( - model: ChatModels, - temperature: Double -) = SimpleActor( - name = "NewFileCreator", - prompt = """ - |Generate the necessary code for new files based on the given requirements and context. - |For each file: - |- Provide a clear relative file path based on the content and purpose of the file. - |- Ensure the code is well-structured, follows best practices, and meets the specified functionality. - |- Carefully consider how the new file fits into the existing project structure and architecture. - |- Avoid creating files that duplicate functionality or introduce inconsistencies. - | - |The response format should be as follows: - |- Use triple backticks to create code blocks for each file. - |- Each code block should be preceded by a header specifying the file path. - |- The file path should be a relative path from the project root. - |- Separate code blocks with a single blank line. - |- Specify the language for syntax highlighting after the opening triple backticks. - | - |Example: - | - |Here are the new files: - | - |### src/utils/exampleUtils.js - |${tripleTilde}js - |// Utility functions for example feature - |const b = 2; - |function exampleFunction() { - | return b + 1; - |} - | - |$tripleTilde - | - |### tests/exampleUtils.test.js - |${tripleTilde}js - |// Unit tests for exampleUtils - |const assert = require('assert'); - |const { exampleFunction } = require('../src/utils/exampleUtils'); - | - |describe('exampleFunction', () => { - | it('should return 3', () => { - | assert.equal(exampleFunction(), 3); - | }); - |}); - |$tripleTilde - """.trimMargin(), - model = model, - temperature = temperature, -) - -private fun patchActor( - model: ChatModels, - temperature: Double -) = SimpleActor( - name = "FilePatcher", - prompt = """ - |Generate a patch for an existing file to modify its functionality or fix issues based on the given requirements and context. - |Ensure the modifications are efficient, maintain readability, and adhere to coding standards. - |Carefully review the existing code and project structure to ensure the changes are consistent and do not introduce bugs. - |Consider the impact of the modifications on other parts of the codebase. - | - |Provide a summary of the changes made. - | - |Response should use one or more code patches in diff format within ${tripleTilde}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 - |${tripleTilde}diff - | // Utility functions for example feature - | const b = 2; - | function exampleFunction() { - |- return b + 1; - |+ return b + 2; - | } - |$tripleTilde - | - |### tests/exampleUtils.test.js - |${tripleTilde}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); - | }); - | }); - |$tripleTilde - """.trimMargin(), - model = model, - temperature = temperature, -) - -private fun inquiryActor( - taskPlanningEnabled: Boolean, - shellCommandTaskEnabled: Boolean, - model: ChatModels, - temperature: Double -) = SimpleActor( - name = "Inquiry", - prompt = """ - Create code for a new file that fulfills the specified requirements and context. - Given a detailed user request, break it down into smaller, actionable tasks suitable for software development. - Compile comprehensive information and insights on the specified topic. - Provide a comprehensive overview, including key concepts, relevant technologies, best practices, and any potential challenges or considerations. - Ensure the information is accurate, up-to-date, and well-organized to facilitate easy understanding. - - When generating insights, consider the existing project context and focus on information that is directly relevant and applicable. - Focus on generating insights and information that support the task types available in the system (Requirements, NewFile, EditFile, ${ - if (!taskPlanningEnabled) "" else "TaskPlanning, " - }${ - if (!shellCommandTaskEnabled) "" else "RunShellCommand, " - }Documentation). - This will ensure that the inquiries are tailored to assist in the planning and execution of tasks within the system's framework. - """.trimIndent(), - model = model, - temperature = temperature, -) - -private fun shellActor( - env: Map, - workingDir: String, - language: String, - command: List, - model: ChatModels, - temperature: Double -) = CodingActor( - name = "RunShellCommand", - interpreterClass = ProcessInterpreter::class, - details = """ - Execute the following shell command(s) and provide the output. Ensure to handle any errors or exceptions gracefully. - - Note: This task is for running simple and safe commands. Avoid executing commands that can cause harm to the system or compromise security. - """.trimIndent(), - symbols = mapOf( - "env" to env, - "workingDir" to File(workingDir).absolutePath, - "language" to language, - "command" to command, - ), - model = model, - temperature = temperature, -) \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt index c94c431b..17f97226 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt @@ -10,7 +10,6 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.vfs.VirtualFile -import com.simiacryptus.diff.FileValidationUtils import com.simiacryptus.diff.FileValidationUtils.Companion.filteredWalk import com.simiacryptus.diff.FileValidationUtils.Companion.isGitignore import com.simiacryptus.diff.FileValidationUtils.Companion.isLLMIncludable diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt index 0ee175af..9880aa81 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -4,6 +4,7 @@ package com.github.simiacryptus.aicoder.config import com.github.simiacryptus.aicoder.ui.SettingsWidgetFactory.SettingsWidget.Companion.isVisible import com.github.simiacryptus.aicoder.util.IdeaOpenAIClient import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.OpenFileDescriptor @@ -13,21 +14,85 @@ import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.ui.SimpleListCellRenderer import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBList import com.intellij.ui.components.JBTextField import com.intellij.ui.table.JBTable import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.models.ImageModels import com.simiacryptus.skyenet.core.platform.ApplicationServices +import java.awt.BorderLayout +import java.awt.Dimension import java.awt.event.ActionEvent import java.io.FileOutputStream -import javax.swing.AbstractAction -import javax.swing.JButton -import javax.swing.JList -import javax.swing.ListCellRenderer +import javax.swing.* +import javax.swing.event.ListSelectionEvent +import javax.swing.event.ListSelectionListener import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableModel class AppSettingsComponent : com.intellij.openapi.Disposable { + val executablesModel = DefaultListModel().apply { + AppSettingsState.instance.executables.forEach { addElement(it) } + } + val executablesList = JBList(executablesModel) + + @Suppress("unused") + @Name("Executables") + val executablesPanel = JPanel(BorderLayout()).apply { + val scrollPane = JScrollPane(executablesList) + scrollPane.preferredSize = Dimension(300, 200) + add(scrollPane, BorderLayout.CENTER) + val buttonPanel = JPanel() + val addButton = JButton("Add") + val removeButton = JButton("Remove") + val editButton = JButton("Edit") + removeButton.isEnabled = false + editButton.isEnabled = false + + addButton.addActionListener { + val descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor() + descriptor.title = "Select Executable" + FileChooser.chooseFile(descriptor, null, null) { file -> + val executablePath = file.path + if (executablePath.isNotBlank() && !executablesModel.contains(executablePath)) { + executablesModel.addElement(executablePath) + AppSettingsState.instance.executables.add(executablePath) + } + } + } + removeButton.addActionListener { + val selectedIndices = executablesList.selectedIndices + for (i in selectedIndices.reversed()) { + val removed = executablesModel.remove(i) + AppSettingsState.instance.executables.remove(removed) + } + } + editButton.addActionListener { + val selectedIndex = executablesList.selectedIndex + if (selectedIndex != -1) { + val currentValue = executablesModel.get(selectedIndex) + val newValue = JOptionPane.showInputDialog(this, "Edit executable path:", currentValue) + if (newValue != null && newValue.isNotBlank()) { + executablesModel.set(selectedIndex, newValue) + AppSettingsState.instance.executables.remove(currentValue) + AppSettingsState.instance.executables.add(newValue) + } + } + } + executablesList.addListSelectionListener(object : ListSelectionListener { + override fun valueChanged(e: ListSelectionEvent?) { + val hasSelection = executablesList.selectedIndex != -1 + removeButton.isEnabled = hasSelection + editButton.isEnabled = hasSelection + } + }) + buttonPanel.add(addButton) + buttonPanel.add(removeButton) + buttonPanel.add(editButton) + add(buttonPanel, BorderLayout.SOUTH) + // Enable multiple selection for the list + executablesList.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION + } @Suppress("unused") @Name("Human Language") @@ -155,6 +220,20 @@ class AppSettingsComponent : com.intellij.openapi.Disposable { var usage = UsageTable(ApplicationServices.usageManager) init { + // Initialize executables list + setExecutables(AppSettingsState.instance.executables) + fun getExecutables(): Set { + fun setExecutables(executables: Set) { + val model = + ((executablesPanel.getComponent(0) as? JScrollPane)?.viewport?.view as? JList)?.model as? DefaultListModel + model?.clear() + executables.forEach { model?.addElement(it) } + } + + val model = + ((executablesPanel.getComponent(0) as? JScrollPane)?.viewport?.view as? JList)?.model as? DefaultListModel + return model?.elements()?.toList()?.toSet() ?: emptySet() + } ChatModels.values() .filter { AppSettingsState.instance.apiKey?.filter { it.value.isNotBlank() }?.keys?.contains(it.value.provider.name) @@ -229,4 +308,17 @@ class AppSettingsComponent : com.intellij.openapi.Disposable { text = value // Here you can add more customization if needed } } + + fun getExecutables(): Set { + val model = + ((executablesPanel.getComponent(0) as? JScrollPane)?.viewport?.view as? JList)?.model as? DefaultListModel + return model?.elements()?.toList()?.toSet() ?: emptySet() + } + + fun setExecutables(executables: Set) { + val model = + ((executablesPanel.getComponent(0) as? JScrollPane)?.viewport?.view as? JList)?.model as? DefaultListModel + model?.clear() + executables.forEach { model?.addElement(it) } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt index 33c52a96..b6e1f160 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt @@ -16,4 +16,4 @@ open class AppSettingsConfigurable : UIAdapter): Array { - return data.flatMap { - (when { - it.name.startsWith(".") -> arrayOf() - isGitignore(it) -> arrayOf() - it.length > 1e6 -> arrayOf() - it.extension?.lowercase(Locale.getDefault()) in - setOf("jar", "zip", "class", "png", "jpg", "jpeg", "gif", "ico") -> arrayOf() - - it.isDirectory -> expandFileList(it.children) - else -> arrayOf(it) - }).toList() - }.toTypedArray() - } - fun isGitignore(file: VirtualFile) = isGitignore(file.toNioPath()) } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt index f8cc0371..0c4c927a 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt @@ -47,7 +47,7 @@ class IdeaOpenAIClient : OpenAIClient( } override fun authorize(request: HttpRequest, apiProvider: APIProvider) { - val checkApiKey = UITools.checkApiKey(key.get(apiProvider)!!) + val checkApiKey = UITools.checkApiKey(key.get(apiProvider) ?: throw IllegalArgumentException("No API Key for $apiProvider")) key = key.toMutableMap().let { it[apiProvider] = checkApiKey it @@ -58,7 +58,7 @@ class IdeaOpenAIClient : OpenAIClient( @Suppress("NAME_SHADOWING") override fun chat( chatRequest: ChatRequest, - model: ChatModels + model: OpenAITextModel ): ChatResponse { lastEvent ?: return super.chat(chatRequest, model) if (isInRequest.getAndSet(true)) { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IndentedText.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IndentedText.kt new file mode 100644 index 00000000..0c7ca6f9 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IndentedText.kt @@ -0,0 +1,45 @@ +package com.github.simiacryptus.aicoder.util + +import com.simiacryptus.jopenai.util.StringUtil + +/** + * This class provides a way to store and manipulate indented text blocks. + * + * The text block is stored as a single string, with each line separated by a newline character. + * The indentation is stored as a separate string, which is prepended to each line when the text block is converted to a string. + * + * The class provides a companion object method to convert a string to an IndentedText object. + * This method replaces all tab characters with two spaces, and then finds the minimum indentation of all lines. + * This indentation is then used as the indentation for the IndentedText object. + * + * The class also provides a method to create a new IndentedText object with a different indentation. + */ +open class IndentedText(var indent: CharSequence, vararg val lines: CharSequence) : TextBlock { + + override fun toString(): String { + return rawString().joinToString(TextBlock.DELIMITER + indent) + } + + override fun withIndent(indent: CharSequence): IndentedText { + return IndentedText(indent, *lines) + } + + override fun rawString(): Array { + return lines + } + + companion object { + /** + * This method is used to convert a string into an IndentedText object. + * + * @param text The string to be converted into an IndentedText object. + * @return IndentedText object created from the input string. + */ + fun fromString(text: String?): IndentedText { + val processedText = (text ?: "").replace("\t", TextBlock.TAB_REPLACEMENT.toString()) + val lines = processedText.split(TextBlock.DELIMITER) + val indent = StringUtil.getWhitespacePrefix(*lines.toTypedArray()) + return IndentedText(indent, *lines.map { StringUtil.stripPrefix(it, indent) }.toTypedArray()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt index 49344579..b7630031 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/LineComment.kt @@ -50,7 +50,7 @@ class LineComment(private val commentPrefix: CharSequence, indent: CharSequence? .collect(Collectors.joining(TextBlock.DELIMITER + indent + commentPrefix + " ")) } - override fun withIndent(indent: CharSequence?): LineComment { + override fun withIndent(indent: CharSequence): LineComment { return LineComment(commentPrefix, indent, *lines) } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/TextBlock.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/TextBlock.kt new file mode 100644 index 00000000..0fc0119e --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/TextBlock.kt @@ -0,0 +1,20 @@ +package com.github.simiacryptus.aicoder.util + +import java.util.stream.Stream +import kotlin.streams.asStream + +interface TextBlock { + companion object { + val TAB_REPLACEMENT: CharSequence = " " + const val DELIMITER: String = "\n" + } + + fun rawString(): Array + + val textBlock: CharSequence + get() = rawString().joinToString(DELIMITER) + + fun withIndent(indent: CharSequence): TextBlock + + fun stream(): Stream = rawString().asSequence().asStream() +} \ No newline at end of file