From df12d1762ed50f31c53498a2d7c1ad4e673b4091 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sat, 12 Oct 2024 21:50:18 -0400 Subject: [PATCH] 1.7.1 --- build.gradle.kts | 7 +- gradle.properties | 6 +- .../aicoder/actions/FileContextAction.kt | 4 + .../actions/code/RecentCodeEditsAction.kt | 2 +- .../generic/GenerateDocumentationAction.kt | 130 ++++++++++---- .../generic/GenerateRelatedFileAction.kt | 19 +- .../actions/generic/MassPatchAction.kt | 21 ++- .../actions/generic/PlanAheadConfigDialog.kt | 10 +- .../aicoder/config/AppSettingsState.kt | 9 +- .../simiacryptus/aicoder/config/MRUItems.kt | 169 ++++++++++++++---- .../aicoder/util/PluginStartupActivity.kt | 2 +- .../simiacryptus/aicoder/util/UITools.kt | 2 +- 12 files changed, 280 insertions(+), 101 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index be7161ad..8c6cb033 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,20 +31,22 @@ repositories { val jetty_version = "11.0.24" val slf4j_version = "2.0.16" -val skyenet_version = "1.2.9" +val skyenet_version = "1.2.10" val remoterobot_version = "0.11.23" val jackson_version = "2.17.2" dependencies { implementation("software.amazon.awssdk:bedrock:2.25.9") implementation("software.amazon.awssdk:bedrockruntime:2.25.9") + implementation("software.amazon.awssdk:s3:2.25.9") + implementation("software.amazon.awssdk:kms:2.25.9") implementation("org.apache.commons:commons-text:1.11.0") implementation(group = "com.vladsch.flexmark", name = "flexmark", version = "0.64.8") implementation("com.googlecode.java-diff-utils:diffutils:1.3.0") implementation(group = "org.apache.httpcomponents.client5", name = "httpclient5", version = "5.2.3") - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.1.8") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.1.9") implementation(group = "com.simiacryptus.skyenet", name = "kotlin", version = skyenet_version) implementation(group = "com.simiacryptus.skyenet", name = "core", version = skyenet_version) implementation(group = "com.simiacryptus.skyenet", name = "webui", version = skyenet_version) @@ -52,6 +54,7 @@ dependencies { implementation(group = "com.fasterxml.jackson.core", name = "jackson-databind", version = jackson_version) implementation(group = "com.fasterxml.jackson.core", name = "jackson-annotations", version = jackson_version) implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = jackson_version) + implementation ("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version") implementation(group = "org.eclipse.jetty", name = "jetty-server", version = jetty_version) implementation(group = "org.eclipse.jetty", name = "jetty-servlet", version = jetty_version) diff --git a/gradle.properties b/gradle.properties index 0f71cebd..ec3460d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.7.0 +pluginVersion=1.7.1 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g @@ -9,8 +9,8 @@ org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g pluginSinceBuild=233 pluginUntilBuild=242.* -platformType = IC -platformVersion=2024.1 +platformType = IU +platformVersion=2024.2 gradleVersion=8.10.2 platformPlugins = diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt index 8ae2f0e8..ba7ac5a6 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt @@ -27,6 +27,10 @@ abstract class FileContextAction( final override fun handle(e: AnActionEvent) { val config = getConfig(e.project, e) + if (config == null) { + log.warn("No configuration found for ${javaClass.simpleName}") + return + } val virtualFile = UITools.getSelectedFile(e) ?: UITools.getSelectedFolder(e) ?: return val project = e.project ?: return val projectRoot = File(project.basePath!!).toPath() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt index bd5328cc..03236bb9 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt @@ -20,7 +20,7 @@ class RecentCodeEditsAction : ActionGroup() { override fun getChildren(e: AnActionEvent?): Array { if (e == null) return emptyArray() val children = mutableListOf() - for ((instruction, _) in AppSettingsState.instance.getRecentCommands("customEdits").mostUsedHistory) { + for (instruction in AppSettingsState.instance.getRecentCommands("customEdits").getMostRecent(10)) { val id = children.size + 1 val text = if (id < 10) "_$id: $instruction" else "$id: $instruction" val element = object : CustomEditAction() { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateDocumentationAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateDocumentationAction.kt index 7c2683f4..47ce926f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateDocumentationAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateDocumentationAction.kt @@ -17,6 +17,7 @@ import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.ui.CheckBoxList import com.intellij.ui.components.JBScrollPane +import javax.swing.JComboBox import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.JBTextField import com.simiacryptus.jopenai.models.ApiModel @@ -30,9 +31,11 @@ import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.Executors import java.util.concurrent.Future +import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.swing.* +import java.util.TreeMap class GenerateDocumentationAction : FileContextAction() { @@ -53,6 +56,9 @@ class GenerateDocumentationAction : FileContextAction() + @Name("Output File") val outputFilename = JBTextField() @@ -73,11 +79,11 @@ class GenerateDocumentationAction : FileContextAction root?.relativize(path)?.toString() ?: path.toString() @@ -87,6 +93,14 @@ class GenerateDocumentationAction : FileContextAction files.filter { path -> settingsUI.filesToProcess.isItemSelected(path) }.toList() + result -> files.filter { path -> settingsUI.filesToProcess.isItemSelected(path) }.sortedBy { it.toString() }.toList() else -> listOf() } + if(settings.filesToProcess.isEmpty()) return null + mruDocumentationInstructions.addInstructionToHistory("${settings.outputFilename} ${settings.transformationMessage}") //.map { path -> return@map root?.resolve(path) }.filterNotNull() return Settings(settings, project) } + private fun updateUIFromSelection(settingsUI: SettingsUI) { + val selected = settingsUI.recentInstructions.selectedItem as? String + if (selected != null) { + val parts = selected.split(" ", limit = 2) + if (parts.size == 2) { + settingsUI.outputFilename.text = parts[0] + settingsUI.transformationMessage.text = parts[1] + } else { + settingsUI.transformationMessage.text = selected + } + } + } + override fun processSelection(state: SelectionState, config: Settings?): Array { + if (config?.settings == null) { + // Dialog was cancelled, return empty array + return emptyArray().also { + // Ensure we don't attempt to open any files when dialog is cancelled + return@also + } + } + val selectedFolder = state.selectedFile.toPath() val gitRoot = TestResultAutofixAction.findGitRoot(selectedFolder) ?: selectedFolder val outputDirectory = config?.settings?.outputDirectory ?: "docs/" var outputPath = selectedFolder.resolve(config?.settings?.outputFilename ?: "compiled_documentation.md") val relativePath = gitRoot.relativize(outputPath) - outputPath = gitRoot.resolve(outputDirectory).resolve(relativePath) + outputPath = gitRoot.resolve(outputDirectory).resolve(relativePath) if (outputPath.toFile().exists()) { val extension = outputPath.toString().split(".").last() val name = outputPath.toString().split(".").dropLast(1).joinToString(".") @@ -118,14 +155,13 @@ class GenerateDocumentationAction : FileContextAction() try { - val selectedPaths = config?.settings?.filesToProcess ?: listOf() + val selectedPaths = (config?.settings?.filesToProcess ?: listOf()).sortedBy { it.toString() } val partitionedPaths = Files.walk(selectedFolder) .filter { Files.isRegularFile(it) && !Files.isDirectory(it) } - .toList().groupBy { selectedPaths.contains(it) } + .toList().sortedBy { it.toString() }.groupBy { selectedPaths.contains(it) } val pathList = partitionedPaths[true] ?.toList()?.filterNotNull() ?.map> { path -> @@ -172,12 +208,19 @@ class GenerateDocumentationAction : FileContextAction + "# $path\n\n$content" + } + outputPath.parent.toFile().mkdirs() + outputPath.parent.toFile().mkdirs() + Files.write(outputPath, sortedContent.toByteArray()) open(config.project!!, outputPath) return arrayOf(outputPath.toFile()) } else { + val outputDir = selectedFolder.resolve(outputDirectory) + outputDir.toFile().mkdirs() open(config?.project!!, selectedFolder.resolve(outputDirectory)) - return pathList.toList().map { it.toFile() }.toTypedArray() + return pathList.map { it.toFile() }.toTypedArray() } } finally { executorService.shutdown() @@ -192,11 +235,10 @@ class GenerateDocumentationAction : FileContextAction ) { if (config?.settings?.singleOutputFile == true) { - markdownContent.append("# ${selectedFolder.relativize(path)}\n\n") - markdownContent.append(transformContent.replace("(?s)(?() init { title = "Compile Documentation" @@ -290,29 +333,39 @@ class GenerateDocumentationAction : FileContextAction settingsUI.filesToProcess.isItemSelected(path) } userSettings.singleOutputFile = settingsUI.singleOutputFile.isSelected } - private fun validateInput(): Boolean { - if (settingsUI.transformationMessage.text.isBlank()) { - Messages.showErrorDialog("AI Instruction cannot be empty", "Input Error") - return false - } - if (settingsUI.outputFilename.text.isBlank()) { - Messages.showErrorDialog("Output File cannot be empty", "Input Error") - return false - } - if (settingsUI.outputDirectory.text.isBlank()) { - Messages.showErrorDialog("Output Directory cannot be empty", "Input Error") - return false - } - return true - } + + private fun validateInput(): Boolean { + if (settingsUI.transformationMessage.text.isBlank()) { + Messages.showErrorDialog("AI Instruction cannot be empty", "Input Error") + return false + } + if (settingsUI.outputFilename.text.isBlank()) { + Messages.showErrorDialog("Output File cannot be empty", "Input Error") + return false + } + if (settingsUI.outputDirectory.text.isBlank()) { + Messages.showErrorDialog("Output Directory cannot be empty", "Input Error") + return false + } + return true + } } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt index 18277484..5b39b2a3 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt @@ -22,13 +22,13 @@ import java.io.File import java.io.FileInputStream import java.nio.file.Path import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference import javax.swing.JTextArea class GenerateRelatedFileAction : FileContextAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun isEnabled(event: AnActionEvent): Boolean { - if (UITools.getSelectedFiles(event).size != 1) return false - return super.isEnabled(event) + return UITools.getSelectedFiles(event).size == 1 && super.isEnabled(event) } data class ProjectFile( @@ -121,7 +121,7 @@ class GenerateRelatedFileAction : FileContextAction Unit - function = { + val functionRef = AtomicReference<(() -> Unit)?>(null) + val function: () -> Unit = { val file = outputPath.toFile() if (file.exists()) { // Ensure the IDE is ready for file operations @@ -154,17 +154,18 @@ class GenerateRelatedFileAction : FileContextAction - var markdown = ui.socketManager?.addApplyFileDiffLinks( - root = root.toPath(), - response = design, - handle = { newCodeMap -> + var markdown = (ui as SocketManagerBase).addApplyFileDiffLinks( + root = _root as Path, + response = design as String, + handle = { newCodeMap:Map -> newCodeMap.forEach { (path, newCode) -> fileTask.complete("$path Updated") } - }, + } as (Map) -> Unit, ui = ui, - api = api, + api = api as API, + shouldAutoApply = { true } as (Path) -> Boolean, ) """
${renderMarkdown(markdown!!)}
""" }, @@ -291,4 +296,4 @@ class MassPatchServer( companion object { val log = org.slf4j.LoggerFactory.getLogger(MassPatchServer::class.java) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadConfigDialog.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadConfigDialog.kt index e705989e..27121f62 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadConfigDialog.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/PlanAheadConfigDialog.kt @@ -90,9 +90,13 @@ class PlanAheadConfigDialog( } override fun getValueAt(row: Int, column: Int): Any = - if (column == 0) { - checkboxStates[row] - } else super.getValueAt(row, column) + try { + if (column == 0) { + checkboxStates[row] + } else super.getValueAt(row, column) + } catch (e: IndexOutOfBoundsException) { + false + } } private val commandTable = JBTable(tableModel).apply { putClientProperty("terminateEditOnFocusLost", true) } private val addCommandButton = JButton("Add Command") diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt index 77c2d0ca..cafd0f4e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -11,6 +11,8 @@ import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.models.ImageModels import com.simiacryptus.jopenai.models.OpenAIModels import com.simiacryptus.util.JsonUtil +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.io.File import java.util.* @@ -47,10 +49,10 @@ data class AppSettingsState( var shellCommand: String = getDefaultShell(), var enableLegacyActions: Boolean = false, var executables: MutableSet = mutableSetOf(), - var recentArguments: MutableList = mutableListOf() + var recentArguments: MutableList = mutableListOf(), + val recentCommands: MutableMap = mutableMapOf(), ) : PersistentStateComponent { private var onSettingsLoadedListeners = mutableListOf<() -> Unit>() - private val recentCommands = mutableMapOf() @JsonIgnore override fun getState(): SimpleEnvelope { @@ -65,7 +67,7 @@ data class AppSettingsState( val fromJson = try { JsonUtil.fromJson(state.value!!, AppSettingsState::class.java) } catch (e: Exception) { - //throw RuntimeException("Error loading settings: ${state.value}", e) + log.warn("Error loading settings: ${state.value}", e) AppSettingsState() } XmlSerializerUtil.copyBean(fromJson, this) @@ -139,6 +141,7 @@ data class AppSettingsState( } companion object { + val log = LoggerFactory.getLogger(AppSettingsState::class.java) var auxiliaryLog: File? = null const val WELCOME_VERSION: String = "1.5.0" diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt index 37483e9f..997d929c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt @@ -1,43 +1,148 @@ package com.github.simiacryptus.aicoder.config -import java.util.Map.Entry.comparingByValue -import java.util.stream.Collectors +import com.fasterxml.jackson.annotation.JsonIgnore +import kotlin.math.min +import java.io.Serializable +import java.time.Instant +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +class MRUItems : Serializable { + + companion object { + const val DEFAULT_LIMIT = 10 + } + + data class HistoryItem(val instruction: String, var usageCount: Int, var lastUsed: Instant) : Serializable + + val history: MutableList = CopyOnWriteArrayList() + + private val lock = ReentrantReadWriteLock() + + var historyLimit = DEFAULT_LIMIT + set(value) { + require(value > 0) { "History limit must be positive" } + lock.write { + field = value + trimHistories() + } + } + + override fun equals(other: Any?): Boolean { + return other is MRUItems && history == other.history + } + + override fun hashCode(): Int { + return history.hashCode() + } -class MRUItems { - val mostUsedHistory: MutableMap = HashMap() - private val mostRecentHistory: MutableList = ArrayList() - private var historyLimit = 10 fun addInstructionToHistory(instruction: CharSequence) { - synchronized(mostRecentHistory) { - if (mostRecentHistory.contains(instruction.toString())) { - mostRecentHistory.remove(instruction.toString()) + lock.write { + val instructionStr = instruction.toString() + val existingItem = history.find { it.instruction == instructionStr } + if (existingItem != null) { + existingItem.usageCount++ + existingItem.lastUsed = Instant.now() + history.remove(existingItem) + history.add(0, existingItem) + } else { + history.add(0, HistoryItem(instructionStr, 1, Instant.now())) } - mostRecentHistory.add(instruction.toString()) - while (mostRecentHistory.size > historyLimit) { - mostRecentHistory.removeAt(0) + trimHistories() + } + } + + @JsonIgnore + fun getMostUsed(limit: Int = DEFAULT_LIMIT): List { + return lock.read { + history + .sortedByDescending { it.usageCount } + .take(min(limit, historyLimit)) + .map { it.instruction } + } + } + + @JsonIgnore + fun getMostRecent(limit: Int = DEFAULT_LIMIT): List { + return lock.read { + history.take(min(limit, historyLimit)).map { it.instruction } + } + } + + @JsonIgnore + fun getMostRecentWithTimestamp(limit: Int = DEFAULT_LIMIT): List> { + return lock.read { + history.take(min(limit, historyLimit)).map { Pair(it.instruction, it.lastUsed) } + } + } + + fun clear() { + lock.write { + history.clear() + } + } + + fun size(): Int = lock.read { history.size } + + fun isEmpty(): Boolean = lock.read { history.isEmpty() } + + fun remove(item: String) { + lock.write { + history.removeIf { it.instruction == item } + } + } + + private fun trimHistories() { + lock.write { + if (history.size > historyLimit) { + history.subList(historyLimit, history.size).clear() } } - synchronized(mostUsedHistory) { - mostUsedHistory.put( - instruction.toString(), - (mostUsedHistory[instruction] ?: 0) + 1 - ) - } - - if (mostUsedHistory.size > historyLimit) { - val retain = mostUsedHistory.entries.stream() - .sorted(comparingByValue().reversed()) - .limit(historyLimit.toLong()) - .map { (key, _) -> key }.collect( - Collectors.toList() - ) - val toRemove = HashSet(mostUsedHistory.keys) - toRemove.removeAll(retain.toSet()) - toRemove.removeAll(mostRecentHistory.toSet()) - toRemove.forEach { key: CharSequence? -> - mostUsedHistory.remove(key) - mostRecentHistory.remove(key.toString()) + } + + fun contains(item: String): Boolean { + return lock.read { + history.any { it.instruction == item } + } + } + + @JsonIgnore + fun getUsageCount(item: String): Int { + return lock.read { history.find { it.instruction == item }?.usageCount ?: 0 } + } + + @JsonIgnore + fun getLastUsedTimestamp(item: String): Instant? { + return lock.read { history.find { it.instruction == item }?.lastUsed } + } + + fun merge(other: MRUItems) { + lock.write { + other.history.forEach { otherItem -> + val existingItem = history.find { it.instruction == otherItem.instruction } + if (existingItem != null) { + existingItem.usageCount += otherItem.usageCount + existingItem.lastUsed = maxOf(existingItem.lastUsed, otherItem.lastUsed) + } else { + history.add(otherItem) + } } + history.sortByDescending { it.lastUsed } + trimHistories() + } + } + + fun removeOlderThan(timestamp: Instant) { + lock.write { + history.removeAll { it.lastUsed.isBefore(timestamp) } + } + } + + override fun toString(): String { + return lock.read { + "MRUItems(mostUsed=${getMostUsed(5)}, mostRecent=${getMostRecent(5)}, size=${history.size})" } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt index 82ade788..e55e6bd6 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt @@ -37,7 +37,7 @@ class PluginStartupActivity : ProjectActivity { val path = resource?.toURI()?.let { java.nio.file.Paths.get(it) } virtualFile = path?.let { VirtualFileManager.getInstance().findFileByNioPath(it) } } catch (e: Exception) { - log.error("Error opening welcome page", e) + log.debug("Error opening welcome page", e) } if (virtualFile == null) { try { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt index ad9e8abc..30a3451d 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt @@ -702,7 +702,7 @@ object UITools { val result = task(indicator) this.result.set(result) } catch (e: Throwable) { - error(log, "Error running task", e) + log.info("Error running task", e) error.set(e) isError.set(true) } finally {