From 3d846d74f039d8486fee83d127f434a4b6a17775 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sun, 16 Jun 2024 22:24:02 -0400 Subject: [PATCH] 1.5.9 (#171) * 1.5.9 * wip * Update build.gradle.kts * Update settings.gradle.kts * autofix * Update CommandAutofixAction.kt * autofix * wip --- build.gradle.kts | 4 +- gradle.properties | 2 +- .../actions/generic/CommandAutofixAction.kt | 402 +++++++++ .../generic/GenerateDocumentationAction.kt | 41 +- .../actions/generic/MultiDiffChatAction.kt | 17 +- ...TypescriptWebDevelopmentAssistantAction.kt | 781 ++++++++++++++++++ .../aicoder/config/AppSettingsState.kt | 6 +- .../config/StaticAppSettingsConfigurable.kt | 161 ++-- .../simiacryptus/aicoder/config/UIAdapter.kt | 36 +- .../aicoder/util/PluginStartupActivity.kt | 2 +- src/main/resources/META-INF/plugin.xml | 7 + 11 files changed, 1345 insertions(+), 114 deletions(-) create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReactTypescriptWebDevelopmentAssistantAction.kt diff --git a/build.gradle.kts b/build.gradle.kts index 86b1777e..d000a8c1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ repositories { val kotlin_version = "2.0.0-Beta5" val jetty_version = "11.0.18" val slf4j_version = "2.0.9" -val skyenet_version = "1.0.75" +val skyenet_version = "1.0.77" val remoterobot_version = "0.11.21" val jackson_version = "2.17.0" @@ -42,7 +42,7 @@ dependencies { exclude(group = "org.jetbrains.kotlin", module = "") } - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.60") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.61") { exclude(group = "org.jetbrains.kotlin", module = "") } diff --git a/gradle.properties b/gradle.properties index 6c5e1eb4..27f07b46 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.5.8 +pluginVersion=1.5.9 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g 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 new file mode 100644 index 00000000..290a05d4 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt @@ -0,0 +1,402 @@ +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.UITools +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +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.intellij.ui.CheckBoxList +import com.intellij.ui.components.JBScrollPane +import com.simiacryptus.diff.addApplyFileDiffLinks +import com.simiacryptus.diff.addSaveLinks +import com.simiacryptus.skyenet.Retryable +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.StorageInterface +import com.simiacryptus.skyenet.core.platform.User +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.awt.Dimension +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import javax.swing.* + +class CommandAutofixAction : BaseAction() { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun handle(event: AnActionEvent) { + val settings = getUserSettings(event) ?: return + var root: Path? = null + val codeFiles: MutableSet = mutableSetOf() + fun codeSummary() = settings.filesToProcess + .filter { it.toFile().exists() } + .joinToString("\n\n") { path -> + """ + |# ${settings.workingDirectory?.toPath()?.relativize(path)} + |```${path.toString().split('.').lastOrNull()} + |${path.toFile().readText(Charsets.UTF_8)} + |``` + """.trimMargin() + } + + val dataContext = event.dataContext + val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) + val folder = UITools.getSelectedFolder(event) + root = if (null != folder) { + folder.toFile.toPath() + } else { + getModuleRootForFile(UITools.getSelectedFile(event)?.parent?.toFile ?: throw RuntimeException("")).toPath() + } + val files = getFiles(virtualFiles, root!!) + codeFiles.addAll(files) + + fun output(): OutputResult = + run { + val command = listOf(settings.executable.absolutePath) + settings.arguments.split(" ") + val processBuilder = ProcessBuilder(command) + processBuilder.directory(settings.workingDirectory) + processBuilder.redirectErrorStream(true) // Merge standard error and standard output + + val process = processBuilder.start() + val exitCode = process.waitFor() + val output = process.inputStream.bufferedReader().readText() + OutputResult(exitCode, output) + } + + val session = StorageInterface.newGlobalID() + val patchApp = PatchApp(root.toFile(), ::codeSummary, codeFiles, ::output, session, settings) + SessionProxyServer.chats[session] = patchApp + val server = AppServer.getServer(event.project) + + Thread { + Thread.sleep(500) + try { + val uri = server.server.uri.resolve("/#$session") + BaseAction.log.info("Opening browser to $uri") + Desktop.getDesktop().browse(uri) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + + + } + + data class OutputResult(val exitCode: Int, val output: String) + inner class PatchApp( + override val root: File, + val codeSummary: () -> String, + val codeFiles: Set = setOf(), + val output: () -> OutputResult, + val session: Session, + val settings: Settings, + ) : ApplicationServer( + applicationName = "Magic Code Fixer", + path = "/fixCmd", + showMenubar = false, + ) { + override val singleInput = true + override val stickyInput = false + override fun newSession(user: User?, session: Session): SocketManager { + val socketManager = super.newSession(user, session) + val ui = (socketManager as ApplicationSocketManager).applicationInterface + val task = ui.newTask() + Retryable( + ui = ui, + task = task, + process = { content -> + val newTask = ui.newTask(false) + newTask.add("Running Command") + Thread { + run(ui, newTask, session, settings) + }.start() + newTask.placeholder + } + ).apply { + set(label(size), process(container)) + } + return socketManager + } + } + + val tripleTilde = "`" + "``" // This is a workaround for the markdown parser when editing this file + + private fun PatchApp.run( + ui: ApplicationInterface, + task: SessionTask, + session: Session, + settings: Settings + ) { + val output = output() + if (output.exitCode == 0 && settings.exitCodeOption == "nonzero") { + task.complete( + """ + |
+ |
Command executed successfully
+ |${renderMarkdown("```\n${output.output}\n```")} + |
+ |""".trimMargin() + ) + return + } + if (settings.exitCodeOption == "zero" && output.exitCode != 0) { + task.complete( + """ + |
+ |
Command failed
+ |${renderMarkdown("```\n${output.output}\n```")} + |
+ |""".trimMargin() + ) + return + } + try { + task.add(""" + |
+ |
Command exit code: ${output.exitCode}
+ |${renderMarkdown("```\n${output.output}\n```")} + |
+ """.trimMargin()) + val response = SimpleActor( + prompt = """ + |You are a helpful AI that helps people with coding. + | + |You will be answering questions about the following code: + | + |${codeSummary()} + | + | + |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} + |""".trimMargin() + ), api = api + ) + var markdown = ui.socketManager?.addApplyFileDiffLinks( + root = root.toPath(), + code = { + val map = codeFiles.associateWith { root.resolve(it.toFile()).readText(Charsets.UTF_8) } + map + }, + response = response, + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + task.complete("$path Updated") + } + }, + ui = ui, + ) + markdown = ui.socketManager?.addSaveLinks( + response = markdown!!, + task = task, + ui = ui, + handle = { path, newCode -> + root.resolve(path.toFile()).writeText(newCode, Charsets.UTF_8) + }, + ) + task.complete("
${renderMarkdown(markdown!!)}
") + } catch (e: Exception) { + task.error(ui, e) + } + } + + data class Settings( + var executable: File, + var arguments: String = "", + var filesToProcess: List = listOf(), + var workingDirectory: File? = null, + var exitCodeOption: String = "0" + ) + + private fun getFiles( + virtualFiles: Array?, root: Path + ): MutableSet { + val codeFiles = mutableSetOf() // Set to avoid duplicates + virtualFiles?.forEach { file -> + if (file.isDirectory) { + codeFiles.addAll(getFiles(file.children, root)) + } else { + codeFiles.add(root.relativize(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()).apply { + filesToProcess.setItems(files.toMutableList()) { path -> + root.relativize(path).toString() + } + files.forEach { path -> + filesToProcess.setItemSelected(path, true) + } + } + 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 + Settings( + executable = executable, + arguments = settingsUI.argumentsField.text, + filesToProcess = files.filter { path -> settingsUI.filesToProcess.isItemSelected(path) } + .map { root.resolve(it) }.toList(), + workingDirectory = File(settingsUI.workingDirectoryField.text), + exitCodeOption = if (settingsUI.exitCodeZero.isSelected) "0" else if (settingsUI.exitCodeAny.isSelected) "any" else "nonzero" + ) + } else { + null + } + } + + class SettingsUI(root: File) { + val argumentsField = JTextField("run build") + val filesToProcess = CheckBoxList() + 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 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") + } + + 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() + } + + override fun createCenterPanel(): JComponent { + val panel = JPanel(BorderLayout()).apply { + val filesScrollPane = JBScrollPane(settingsUI.filesToProcess).apply { + preferredSize = Dimension(400, 300) // Adjust the preferred size as needed + } + add(JLabel("Files to Process"), BorderLayout.NORTH) + add(filesScrollPane, BorderLayout.CENTER) // Make the files list the dominant element + + 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(optionsPanel, BorderLayout.SOUTH) + } + return panel + } + } + + override fun isEnabled(event: AnActionEvent) = true + + companion object { + private val log = LoggerFactory.getLogger(CommandAutofixAction::class.java) + } +} \ No newline at end of file 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 fb48ee4a..6bb1c413 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 @@ -28,10 +28,7 @@ import java.nio.file.Path import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.TimeUnit -import javax.swing.BoxLayout -import javax.swing.JComponent -import javax.swing.JLabel -import javax.swing.JPanel +import javax.swing.* class GenerateDocumentationAction : FileContextAction() { @@ -43,6 +40,9 @@ class GenerateDocumentationAction : FileContextAction() @@ -57,6 +57,7 @@ class GenerateDocumentationAction : FileContextAction = listOf(), + var singleOutputFile: Boolean = true ) class Settings( @@ -79,8 +80,9 @@ class GenerateDocumentationAction : FileContextAction files.filter { path -> settingsUI.filesToProcess.isItemSelected(path) }.toList() else -> listOf() @@ -116,22 +118,32 @@ class GenerateDocumentationAction : FileContextAction try { - future.get() ?: return@map null + future.get() } catch (e: Exception) { log.warn("Error processing file", e) return@map null } }.filterNotNull() - Files.write(outputPath, markdownContent.toString().toByteArray()) - open(config?.project!!, outputPath) - return arrayOf(outputPath.toFile()) + if (config?.settings?.singleOutputFile == true) { + Files.write(outputPath, markdownContent.toString().toByteArray()) + open(config?.project!!, outputPath) + return arrayOf(outputPath.toFile()) + } else { + open(config?.project!!, outputPath) + return pathList.toList().map { it.toFile() }.toTypedArray() + } } finally { executorService.shutdown() } @@ -199,7 +211,6 @@ class GenerateDocumentationAction : FileContextAction settingsUI.filesToProcess.isItemSelected(path) } + userSettings.singleOutputFile = settingsUI.singleOutputFile.isSelected } } } @@ -233,4 +246,4 @@ val CheckBoxList.items: List items.add(getItemAt(i)!!) } return items - } + } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt index 14873dc9..ec73d789 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt @@ -91,8 +91,14 @@ class MultiDiffChatAction : BaseAction() { ) { override val singleInput = false override val stickyInput = true - private val mainActor: SimpleActor - get() = SimpleActor( + override fun userMessage( + session: Session, + user: User?, + userMessage: String, + ui: ApplicationInterface, + api: API + ) { + val mainActor = SimpleActor( prompt = """ |You are a helpful AI that helps people with coding. | @@ -138,13 +144,6 @@ class MultiDiffChatAction : BaseAction() { model = AppSettingsState.instance.defaultSmartModel() ) - override fun userMessage( - session: Session, - user: User?, - userMessage: String, - ui: ApplicationInterface, - api: API - ) { val settings = getSettings(session, user) ?: Settings() if (api is ClientManager.MonitoredClient) api.budget = settings.budget ?: 2.00 diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReactTypescriptWebDevelopmentAssistantAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReactTypescriptWebDevelopmentAssistantAction.kt new file mode 100644 index 00000000..ecef0eea --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReactTypescriptWebDevelopmentAssistantAction.kt @@ -0,0 +1,781 @@ +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.UITools +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.vfs.VirtualFile +import com.simiacryptus.diff.addApplyFileDiffLinks +import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.describe.Description +import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.ImageModels +import com.simiacryptus.jopenai.proxy.ValidatedObject +import com.simiacryptus.jopenai.util.ClientUtil.toContentList +import com.simiacryptus.jopenai.util.JsonUtil +import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.Discussable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.* +import com.simiacryptus.skyenet.core.platform.ClientManager +import com.simiacryptus.skyenet.core.platform.Session +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.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.Desktop +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.file.Path +import java.util.concurrent.Semaphore +import java.util.concurrent.atomic.AtomicReference +import javax.imageio.ImageIO +import kotlin.io.path.name + +// +//val VirtualFile.toFile: File get() = File(this.path) + +class ReactTypescriptWebDevelopmentAssistantAction : BaseAction() { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + val path = "/webDev" + + override fun handle(e: AnActionEvent) { + val session = StorageInterface.newGlobalID() + val selectedFile = UITools.getSelectedFolder(e) + if (null != selectedFile) { + DataStorage.sessionPaths[session] = selectedFile.toFile + } + SessionProxyServer.chats[session] = WebDevApp(root = selectedFile) + val server = AppServer.getServer(e.project) + + Thread { + Thread.sleep(500) + try { + val uri = server.server.uri.resolve("/#$session") + BaseAction.log.info("Opening browser to $uri") + Desktop.getDesktop().browse(uri) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + override fun isEnabled(event: AnActionEvent): Boolean { + if (UITools.getSelectedFile(event)?.isDirectory == false) return false + return super.isEnabled(event) + } + + open class WebDevApp( + applicationName: String = "Web Development Agent", + val temperature: Double = 0.1, + root: VirtualFile?, + override val singleInput: Boolean = false, + ) : ApplicationServer( + applicationName = applicationName, + path = "/webdev", + showMenubar = false, + root = root?.toFile!!, + ) { + override fun userMessage( + session: Session, + user: User?, + userMessage: String, + ui: ApplicationInterface, + api: API + ) { + val settings = getSettings(session, user) ?: Settings() + if (api is ClientManager.MonitoredClient) api.budget = settings.budget ?: 2.00 + WebDevAgent( + api = api, + dataStorage = dataStorage, + session = session, + user = user, + ui = ui, + tools = settings.tools, + model = settings.model, + parsingModel = settings.parsingModel, + root = root, + ).start( + userMessage = userMessage, + ) + } + + data class Settings( + val budget: Double? = 2.00, + val tools: List = emptyList(), + val model: ChatModels = ChatModels.GPT4o, + val parsingModel: ChatModels = ChatModels.GPT35Turbo, + ) + + override val settingsClass: Class<*> get() = Settings::class.java + + @Suppress("UNCHECKED_CAST") + override fun initSettings(session: Session): T? = Settings() as T + } + + class WebDevAgent( + val api: API, + dataStorage: StorageInterface, + session: Session, + user: User?, + val ui: ApplicationInterface, + val model: ChatModels, + val parsingModel: ChatModels, + val tools: List = emptyList(), + actorMap: Map> = mapOf( + ActorTypes.ArchitectureDiscussionActor to ParsedActor( + resultClass = ProjectSpec::class.java, + prompt = """ + Translate the user's idea into a detailed architecture for a simple web application using React and TypeScript. + + List all html, css, typescript, and image files to be created, and for each file: + Translate the user's idea into a detailed architecture for a simple web application. + + List all html, css, javascript, and image files to be created, and for each file: + 1. Mark with filename tags. + 2. Describe the public interface / interaction with other components. + 3. Core functional requirements. + + Specify user interactions and how the application will respond to them. + Identify key HTML classes and element IDs that will be used to bind the application to the HTML. + """.trimIndent(), + model = model, + parsingModel = parsingModel, + ), + ActorTypes.CodeReviewer to SimpleActor( + prompt = """ +Analyze the code summarized in the user's header-labeled code blocks. +Review, look for bugs, and provide fixes. +Provide implementations for missing functions. + +Response should use one or more code patches in diff format within ```diff code blocks. +Each diff should be preceded by a header that identifies the file being modified. +The diff format should use + for line additions, - for line deletions. +The diff should include 2 lines of context before and after every change. + +Example: + +Here are the patches: + +### src/utils/exampleUtils.ts +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +### tests/exampleUtils.test.ts +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |Analyze the code summarized in the user's header-labeled code blocks. + |Review, look for bugs, and provide fixes. + |Provide implementations for missing functions. + | + |Response should use one or more code patches in diff format within ```diff code blocks. + |Each diff should be preceded by a header that identifies the file being modified. + |The diff format should use + for line additions, - for line deletions. + |The diff should include 2 lines of context before and after every change. + | + |Example: + | + |Here are the patches: + | + |### src/utils/exampleUtils.js + |```diff + | // Utility functions for example feature + | const b = 2; + | function exampleFunction() { + |- return b + 1; + |+ return b + 2; + | } + |``` + | + |### tests/exampleUtils.test.js + |```diff + | // Unit tests for exampleUtils + | const assert = require('assert'); + | const { exampleFunction } = require('../src/utils/exampleUtils'); + | + | describe('exampleFunction', () => { + |- it('should return 3', () => { + |+ it('should return 4', () => { + | assert.equal(exampleFunction(), 3); + | }); + | }); + |``` + """.trimMargin(), + model = model, + ), + ActorTypes.HtmlCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a skeleton HTML file for a rich React application. + The html file can reference needed CSS and JS files, which will be located in the same directory as the html file. + You will translate the user request into a skeleton HTML file for a rich javascript application. + The html file can reference needed CSS and JS files, which are will be located in the same directory as the html file. + Do not output the content of the resource files, only the html file. + """.trimIndent(), model = model + ), + ActorTypes.TypescriptCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a TypeScript file for use in a React application. + """.trimIndent(), model = model + ), + /*ActorTypes.JavascriptCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a javascript file for use in a rich javascript application. + """.trimIndent(), model = model + ),*/ + ActorTypes.CssCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a CSS file for use in a React application. + """.trimIndent(), model = model + ), + ActorTypes.EtcCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a file for use in a web application. + """.trimIndent(), + model = model, + ), + ActorTypes.ImageActor to ImageActor( + prompt = """ + You will translate the user request into an image file for use in a web application. + """.trimIndent(), + textModel = model, + imageModel = ImageModels.DallE3, + ), + ), + val root: File, + ) : + ActorSystem( + actorMap.map { it.key.name to it.value }.toMap(), + dataStorage, + user, + session + ) { + enum class ActorTypes { + HtmlCodingActor, + TypescriptCodingActor, + CssCodingActor, + ArchitectureDiscussionActor, + CodeReviewer, + EtcCodingActor, + ImageActor, + } + + private val architectureDiscussionActor by lazy { getActor(ActorTypes.ArchitectureDiscussionActor) as ParsedActor } + private val htmlActor by lazy { getActor(ActorTypes.HtmlCodingActor) as SimpleActor } + private val imageActor by lazy { getActor(ActorTypes.ImageActor) as ImageActor } + private val typescriptActor by lazy { getActor(ActorTypes.TypescriptCodingActor) as SimpleActor } + private val cssActor by lazy { getActor(ActorTypes.CssCodingActor) as SimpleActor } + private val codeReviewer by lazy { getActor(ActorTypes.CodeReviewer) as SimpleActor } + private val etcActor by lazy { getActor(ActorTypes.EtcCodingActor) as SimpleActor } + + private val codeFiles = mutableSetOf() + + fun start( + userMessage: String, + ) { + val task = ui.newTask() + val toInput = { it: String -> listOf(it) } + val architectureResponse = Discussable( + task = task, + userMessage = { userMessage }, + initialResponse = { it: String -> architectureDiscussionActor.answer(toInput(it), api = api) }, + outputFn = { design: ParsedResponse -> + AgentPatterns.displayMapInTabs( + mapOf( + "Text" to renderMarkdown(design.text, ui = ui), + "JSON" to renderMarkdown( + "```json\n${JsonUtil.toJson(design.obj)}\n```", + ui = ui + ), + ) + ) + }, + ui = ui, + reviseResponse = { userMessages: List> -> + architectureDiscussionActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput(userMessage), + api = api + ) + }, + atomicRef = AtomicReference(), + semaphore = Semaphore(0), + heading = userMessage + ).call() + + + try { +// val toolSpecs = tools.map { ToolServlet.tools.find { t -> t.path == it } } +// .joinToString("\n\n") { it?.let { JsonUtil.toJson(it.openApiDescription) } ?: "" } + var messageWithTools = userMessage + task.echo( + renderMarkdown( + "```json\n${JsonUtil.toJson(architectureResponse.obj)}\n```", + ui = ui + ) + ) + val fileTabs = TabbedDisplay(task) + architectureResponse.obj.files.filter { + !it.name!!.startsWith("http") + }.map { (path, description) -> + val task = ui.newTask(false).apply { fileTabs[path.toString()] = placeholder } + task.header("Drafting $path") + codeFiles.add(File(path).toPath()) + pool.submit { + val extension = path!!.split(".").last().lowercase() + when (extension) { + + "ts", "tsx", "js" -> draftResourceCode( + task = task, + request = typescriptActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + actor = typescriptActor, + path = File(path).toPath(), extension, "typescript" + ) + + "css" -> draftResourceCode( + task = task, + request = cssActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + actor = cssActor, + path = File(path).toPath() + ) + + "html" -> draftResourceCode( + task = task, + request = htmlActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + actor = htmlActor, + path = File(path).toPath() + ) + + "png" -> draftImage( + task = task, + request = etcActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + actor = imageActor, + path = File(path).toPath() + ) + + "jpg" -> draftImage( + task = task, + request = etcActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + actor = imageActor, + path = File(path).toPath() + ) + + else -> draftResourceCode( + task = task, + request = etcActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + actor = etcActor, + path = File(path).toPath() + ) + + } + } + }.toTypedArray().forEach { it.get() } + // Apply codeReviewer + iterateCode(task) + } catch (e: Throwable) { + log.warn("Error", e) + task.error(ui, e) + } + + } + + + fun codeSummary() = codeFiles.filter { + if (it.name.lowercase().endsWith(".png")) return@filter false + if (it.name.lowercase().endsWith(".jpg")) return@filter false + true + }.joinToString("\n\n") { path -> + "# $path\n```${path.toString().split('.').last()}\n${root.resolve(path.toFile()).readText()}\n```" + } + + private fun iterateCode( + task: SessionTask + ) { + Discussable( + task = task, + heading = "Code Refinement", + userMessage = { codeSummary() }, + initialResponse = { + codeReviewer.answer(listOf(it), api = api) + }, + outputFn = { code -> + renderMarkdown( + ui.socketManager!!.addApplyFileDiffLinks( + root = root.toPath(), + code = { + codeFiles.filter { + if (it.name.lowercase().endsWith(".png")) return@filter false + if (it.name.lowercase().endsWith(".jpg")) return@filter false + true + }.map { it to root.resolve(it.toFile()).readText() }.toMap() + }, + response = code, + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + task.complete("$path Updated") + } + }, + ui = ui + ) + ) + }, + ui = ui, + reviseResponse = { userMessages -> + val userMessages = userMessages.toMutableList() + userMessages.set(0, userMessages.get(0).copy(first = codeSummary())) + val combinedMessages = + userMessages.map { ApiModel.ChatMessage(Role.user, it.first.toContentList()) } + codeReviewer.respond( + input = listOf(element = combinedMessages.joinToString("\n")), + api = api, + messages = combinedMessages.toTypedArray(), + ) + }, + ).call() + } + + private fun draftImage( + task: SessionTask, + request: Array, + actor: ImageActor, + path: Path, + ) { + try { + var code = Discussable( + task = task, + userMessage = { "" }, + heading = "Drafting $path", + initialResponse = { + val messages = (request + ApiModel.ChatMessage(Role.user, "Draft $path".toContentList())) + .toList().toTypedArray() + actor.respond( + listOf(request.joinToString("\n") { it.content?.joinToString() ?: "" }), + api, + *messages + ) + + }, + outputFn = { img -> + renderMarkdown( + "", ui = ui + ) + }, + ui = ui, + reviseResponse = { userMessages: List> -> + actor.respond( + messages = (request.toList() + userMessages.map { + ApiModel.ChatMessage( + it.second, + it.first.toContentList() + ) + }) + .toTypedArray(), + input = listOf(element = (request.toList() + userMessages.map { + ApiModel.ChatMessage( + it.second, + it.first.toContentList() + ) + }) + .joinToString("\n") { it.content?.joinToString() ?: "" }), + api = api, + ) + }, + ).call() + task.complete( + renderMarkdown( + "", ui = ui + ) + ) + } catch (e: Throwable) { + val error = task.error(ui, e) + task.complete(ui.hrefLink("♻", "href-link regen-button") { + error?.clear() + draftImage(task, request, actor, path) + }) + } + } + + private fun write( + code: ImageResponse, + path: Path + ): ByteArray { + val byteArrayOutputStream = ByteArrayOutputStream() + ImageIO.write( + code.image, + path.toString().split(".").last(), + byteArrayOutputStream + ) + val bytes = byteArrayOutputStream.toByteArray() + return bytes + } + + private fun draftResourceCode( + task: SessionTask, + request: Array, + actor: SimpleActor, + path: Path, + vararg languages: String = arrayOf(path.toString().split(".").last().lowercase()), + ) { + try { + var code = Discussable( + task = task, + userMessage = { "Drafting $path" }, + heading = "", + initialResponse = { + actor.respond( + listOf(request.joinToString("\n") { it.content?.joinToString() ?: "" }), + api, + *(request + ApiModel.ChatMessage(Role.user, "Draft $path".toContentList())) + .toList().toTypedArray() + ) + }, + outputFn = { design: String -> + var design = design + languages.forEach { language -> + if (design.contains("```$language")) { + design = design.substringAfter("```$language").substringBefore("```") + } + } + renderMarkdown("```${languages.first()}\n${design.let { it }}\n```", ui = ui) + }, + ui = ui, + reviseResponse = { userMessages: List> -> + actor.respond( + messages = (request.toList() + userMessages.map { + ApiModel.ChatMessage( + it.second, + it.first.toContentList() + ) + }) + .toTypedArray(), + input = listOf(element = (request.toList() + userMessages.map { + ApiModel.ChatMessage( + it.second, + it.first.toContentList() + ) + }) + .joinToString("\n") { it.content?.joinToString() ?: "" }), + api = api, + ) + }, + ).call() + code = extractCode(code) + task.complete( + "$path Updated" + ) + } catch (e: Throwable) { + val error = task.error(ui, e) + task.complete(ui.hrefLink("♻", "href-link regen-button") { + error?.clear() + draftResourceCode(task, request, actor, path, *languages) + }) + } + } + + private fun extractCode(code: String): String { + var code = code + code = code.trim() + "(?s)```[^\\n]*\n(.*)\n```".toRegex().find(code)?.let { + code = it.groupValues[1] + } + return code + } + + } + + companion object { + private val log = LoggerFactory.getLogger(WebDevelopmentAssistantAction::class.java) + val root: File get() = File(AppSettingsState.instance.pluginHome, "code_chat") + + data class ProjectSpec( + @Description("Files in the project design, including all local html, css, and js files.") + val files: List = emptyList() + ) : ValidatedObject { + override fun validate(): String? = when { + files.isEmpty() -> "Resources are required" + files.any { it.validate() != null } -> "Invalid resource" + else -> null + } + } + + data class ProjectFile( + @Description("The path to the file, relative to the project root.") + val name: String? = "", + @Description("A brief description of the file's purpose and contents.") + val description: String? = "" + ) : ValidatedObject { + override fun validate(): String? = when { + name.isNullOrBlank() -> "Path is required" + name.contains(" ") -> "Path cannot contain spaces" + !name.contains(".") -> "Path must contain a file extension" + else -> null + } + } + + } + +} \ No newline at end of file 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 ee64a211..6b43e98e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.config +package com.github.simiacryptus.aicoder.config import com.fasterxml.jackson.annotation.JsonIgnore import com.intellij.openapi.application.ApplicationManager @@ -43,6 +43,7 @@ data class AppSettingsState( var greetedVersion: String = "", var shellCommand: String = getDefaultShell(), var enableLegacyActions: Boolean = false, + var executables: MutableSet = mutableSetOf() ) : PersistentStateComponent { private var onSettingsLoadedListeners = mutableListOf<() -> Unit>() private val recentCommands = mutableMapOf() @@ -108,6 +109,7 @@ data class AppSettingsState( if (greetedVersion != other.greetedVersion) return false if (mainImageModel != other.mainImageModel) return false if (enableLegacyActions != other.enableLegacyActions) return false + if (executables != other.executables) return false return true } @@ -133,6 +135,8 @@ data class AppSettingsState( result = 31 * result + showWelcomeScreen.hashCode() result = 31 * result + greetedVersion.hashCode() result = 31 * result + mainImageModel.hashCode() + result = 31 * result + enableLegacyActions.hashCode() + result = 31 * result + executables.hashCode() return result } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/StaticAppSettingsConfigurable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/StaticAppSettingsConfigurable.kt index 48db3508..025e825c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/StaticAppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/StaticAppSettingsConfigurable.kt @@ -32,92 +32,103 @@ class StaticAppSettingsConfigurable : AppSettingsConfigurable() { override fun build(component: AppSettingsComponent): JComponent { val tabbedPane = com.intellij.ui.components.JBTabbedPane() - // Basic Settings Tab - val basicSettingsPanel = JPanel(BorderLayout()).apply { - add(JPanel(BorderLayout()).apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Smart Model:")) - add(component.smartModel) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Fast Model:")) - add(component.fastModel) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Main Image Model:")) - add(component.mainImageModel) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Temperature:")) - add(component.temperature) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Human Language:")) - add(component.humanLanguage) - }) + try {// Basic Settings Tab + val basicSettingsPanel = JPanel(BorderLayout()).apply { add(JPanel(BorderLayout()).apply { - add(JLabel("API Configurations:"), BorderLayout.NORTH) - add(component.apis, BorderLayout.CENTER) + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Smart Model:")) + add(component.smartModel) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Fast Model:")) + add(component.fastModel) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Main Image Model:")) + add(component.mainImageModel) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Temperature:")) + add(component.temperature) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Human Language:")) + add(component.humanLanguage) + }) + add(JPanel(BorderLayout()).apply { + add(JLabel("API Configurations:"), BorderLayout.NORTH) + add(component.apis, BorderLayout.CENTER) + }) }) - }) + } + tabbedPane.addTab("Basic Settings", basicSettingsPanel) + } catch (e: Exception) { + log.warn("Error building Basic Settings", e) } - tabbedPane.addTab("Basic Settings", basicSettingsPanel) tabbedPane.addTab("Developer Tools", JPanel(BorderLayout()).apply { - add(JPanel().apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Developer Tools:")) - add(component.devActions) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Enable Legacy Actions:")) - add(component.enableLegacyActions) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - // Removed sections that reference non-existing components + try { + add(JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Developer Tools:")) + add(component.devActions) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Enable Legacy Actions:")) + add(component.enableLegacyActions) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + // Removed sections that reference non-existing components + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Ignore Errors:")) + add(component.suppressErrors) + }) + }, BorderLayout.NORTH) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Edit API Requests:")) + add(component.editRequests) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Enable API Log:")) + add(component.apiLog) + add(component.openApiLog) + add(component.clearApiLog) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Server Port:")) + add(component.listeningPort) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Server Endpoint:")) + add(component.listeningEndpoint) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Plugin Home:")) + add(component.pluginHome) + add(component.choosePluginHome) + }) add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Ignore Errors:")) - add(component.suppressErrors) + add(JLabel("Shell Command:")) + add(component.shellCommand) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + //add(JLabel("Show Welcome Screen:")) + add(component.showWelcomeScreen) }) }, BorderLayout.NORTH) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Edit API Requests:")) - add(component.editRequests) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Enable API Log:")) - add(component.apiLog) - add(component.openApiLog) - add(component.clearApiLog) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Server Port:")) - add(component.listeningPort) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Server Endpoint:")) - add(component.listeningEndpoint) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Plugin Home:")) - add(component.pluginHome) - add(component.choosePluginHome) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - add(JLabel("Shell Command:")) - add(component.shellCommand) - }) - add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { - //add(JLabel("Show Welcome Screen:")) - add(component.showWelcomeScreen) - }) - }, BorderLayout.NORTH) + } catch (e: Exception) { + log.warn("Error building Developer Tools", e) + } }) tabbedPane.addTab("Usage", JPanel(BorderLayout()).apply { - add(component.usage, BorderLayout.CENTER) + try { + add(component.usage, BorderLayout.CENTER) + } catch (e: Exception) { + log.warn("Error building Usage", e) + } }) return tabbedPane diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt index be80365f..b03181b8 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt @@ -3,6 +3,7 @@ package com.github.simiacryptus.aicoder.config import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.Disposable import com.intellij.openapi.options.Configurable +import org.slf4j.LoggerFactory import javax.swing.JComponent abstract class UIAdapter( @@ -10,6 +11,10 @@ abstract class UIAdapter( protected var component: C? = null, ) : Configurable { + companion object { + private val log = LoggerFactory.getLogger(UIAdapter::class.java) + } + @Volatile private var mainPanel: JComponent? = null override fun getDisplayName(): String { @@ -22,10 +27,14 @@ abstract class UIAdapter( if (null == mainPanel) { synchronized(this) { if (null == mainPanel) { - val component = newComponent() - this.component = component - mainPanel = build(component) - write(settingsInstance, component) + try { + val component = newComponent() + this.component = component + mainPanel = build(component) + write(settingsInstance, component) + } catch (e: Exception) { + log.error("Error creating component", e) + } } } } @@ -34,15 +43,20 @@ abstract class UIAdapter( abstract fun newComponent(): C abstract fun newSettings(): S - private fun getSettings(component: C? = this.component) = when (component) { - null -> settingsInstance - else -> { - val buffer = newSettings() - read(component, buffer) - buffer + private fun getSettings(component: C? = this.component) = try { + when (component) { + null -> settingsInstance + else -> { + val buffer = newSettings() + read(component, buffer) + buffer + } } + } catch (e: Exception) { + log.error("Error reading settings", e) + settingsInstance } - + override fun isModified() = when { component == null -> false getSettings() != settingsInstance -> true 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 753ce828..6267d760 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt @@ -91,7 +91,7 @@ class PluginStartupActivity : ProjectActivity { override fun createClient(session: Session, user: User?) = IdeaOpenAIClient.instance } - ApplicationServices.usageManager = UsageManager(File(AppSettingsState.instance.pluginHome, "usage")) + ApplicationServices.usageManager = HSQLUsageManager(ApplicationServicesConfig.dataStorageRoot.resolve("usage")) ApplicationServices.authorizationManager = object : AuthorizationInterface { override fun isAuthorized( applicationClass: Class<*>?, diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 3b0610b0..72990c45 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -89,6 +89,13 @@ + + + + +