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(" $taskId \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