diff --git a/build.gradle.kts b/build.gradle.kts index afec6a08..d3ceb2a2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,11 +25,13 @@ 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.71" +val skyenet_version = "1.0.74" val remoterobot_version = "0.11.21" val jackson_version = "2.17.0" dependencies { + implementation("software.amazon.awssdk:bedrock:2.25.7") + implementation("software.amazon.awssdk:bedrockruntime:2.25.7") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") @@ -40,7 +42,7 @@ dependencies { exclude(group = "org.jetbrains.kotlin", module = "") } - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.56") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.57") { exclude(group = "org.jetbrains.kotlin", module = "") } diff --git a/gradle.properties b/gradle.properties index 9a427f07..88d4454b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.5.4 +pluginVersion=1.5.5 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt index 6f4a536a..b72498a7 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt @@ -1,5 +1,6 @@ package com.github.simiacryptus.aicoder +import com.github.simiacryptus.aicoder.actions.generic.SessionProxyServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.progress.ProgressIndicator @@ -26,7 +27,9 @@ class AppServer( server } - private val handlers = arrayOf().toMutableList() + private val handlers = arrayOf( + newWebAppContext(SessionProxyServer(), "/") + ).toMutableList() private val contexts by lazy { val contexts = ContextHandlerCollection() @@ -34,24 +37,6 @@ class AppServer( contexts } - val appRegistry = mutableMapOf() - - @Synchronized - fun addApp(path: String, socketServer: ChatServer) { - try { - synchronized(serverLock) { - appRegistry[path] = socketServer - if (server.isRunning) server.stop() // Stop the server - handlers += newWebAppContext(socketServer, path) - contexts.handlers = handlers.toTypedArray() - server.handler = contexts - server.start() // Start the server again to reflect the new context - } - } catch (e: Exception) { - log.error("Error while restarting the server with new context", e) - } - } - private fun newWebAppContext(server: ChatServer, path: String): WebAppContext { val context = WebAppContext() JettyWebSocketServletContainerInitializer.configure(context, null) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt index b7c12b10..d65ed879 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt @@ -2,6 +2,8 @@ import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.AppServer +import com.github.simiacryptus.aicoder.actions.BaseAction.Companion +import com.github.simiacryptus.aicoder.actions.generic.SessionProxyServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.chatModel import com.github.simiacryptus.aicoder.util.ComputerLanguage @@ -10,18 +12,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.fileEditor.FileDocumentManager import com.simiacryptus.skyenet.core.platform.ApplicationServices -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.ApplicationServer -import com.simiacryptus.skyenet.webui.chat.ChatServer import com.simiacryptus.skyenet.webui.chat.ChatSocketManager 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.Desktop -import java.io.File class LineFilterChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -38,7 +36,7 @@ class LineFilterChatAction : BaseAction() { val codelines = lines.withIndex().joinToString("\n") { (i, line) -> "${i.toString().padStart(3, '0')} $line" } - agents[session] = object : ChatSocketManager( + SessionProxyServer.agents[session] = object : ChatSocketManager( session = session, model = AppSettingsState.instance.smartModel.chatModel(), userInterfacePrompt = """ @@ -74,7 +72,7 @@ class LineFilterChatAction : BaseAction() { """.trimMargin(), api = api, applicationClass = ApplicationServer::class.java, - storage = ApplicationServices.dataStorageFactory(root), + storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome), ) { override fun canWrite(user: User?): Boolean = true override fun renderResponse(response: String, task: SessionTask): String { @@ -91,13 +89,13 @@ class LineFilterChatAction : BaseAction() { } val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + 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) } @@ -108,22 +106,6 @@ class LineFilterChatAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(LineFilterChatAction::class.java) - private val agents = mutableMapOf() - val root: File get() = File(AppSettingsState.instance.pluginHome, "code_chat") - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "Code Chat", - path = path, - showMenubar = false, - ) { - override val singleInput = false - override val stickyInput = true - override fun newSession(user: User?, session: Session) = agents[session]!! - } - server.addApp(path, socketServer) - return socketServer - } } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt index d5757a9f..2a16f05b 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt @@ -2,6 +2,7 @@ import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.AppServer +import com.github.simiacryptus.aicoder.actions.BaseAction.Companion import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.chatModel import com.github.simiacryptus.aicoder.util.CodeChatSocketManager @@ -11,15 +12,10 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.fileEditor.FileDocumentManager import com.simiacryptus.skyenet.core.platform.ApplicationServices -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.ApplicationServer -import com.simiacryptus.skyenet.webui.chat.ChatServer -import com.simiacryptus.skyenet.webui.session.SocketManager import org.slf4j.LoggerFactory import java.awt.Desktop -import java.io.File class CodeChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -32,24 +28,32 @@ class CodeChatAction : BaseAction() { val session = StorageInterface.newGlobalID() val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return val filename = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: return - agents[session] = CodeChatSocketManager( + SessionProxyServer.agents[session] = CodeChatSocketManager( session = session, language = language, codeSelection = editor.caretModel.primaryCaret.selectedText ?: editor.document.text, filename = filename, api = api, model = AppSettingsState.instance.smartModel.chatModel(), - storage = ApplicationServices.dataStorageFactory(root) + storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome) + ) + ApplicationServer.sessionAppInfoMap[session.toString()] = mapOf( + "applicationName" to "Code Chat", + "singleInput" to false, + "stickyInput" to true, + "loadImages" to false, + "showMenubar" to false, ) val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + 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) } @@ -60,23 +64,5 @@ class CodeChatAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(CodeChatAction::class.java) - private val agents = mutableMapOf() - val root: File get() = File(AppSettingsState.instance.pluginHome, "code_chat") - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "Code Chat", - path = path, - showMenubar = false, - ) { - override val singleInput = false - override val stickyInput = true - override fun newSession(user: User?, session: Session) = - agents[session] ?: throw IllegalArgumentException("Unknown session: $session") - } - server.addApp(path, socketServer) - return socketServer - } - } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateImageAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateImageAction.kt index 97ed1d86..156d56c6 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateImageAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateImageAction.kt @@ -3,6 +3,7 @@ import ai.grazie.utils.mpp.UUID import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.actions.BaseAction.Companion import com.github.simiacryptus.aicoder.actions.generic.MultiStepPatchAction.AutoDevApp.Settings import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.imageModel @@ -15,7 +16,6 @@ import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.ApiModel.Role import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.models.ImageModels import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.skyenet.Discussable import com.simiacryptus.skyenet.core.actors.* @@ -23,7 +23,6 @@ import com.simiacryptus.skyenet.core.platform.* 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.chat.ChatServer import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory import java.awt.Desktop @@ -60,7 +59,7 @@ class CreateImageAction : BaseAction() { val folder = UITools.getSelectedFolder(event) root = if (null != folder) { folder.toFile.toPath() - } else if (1 == virtualFiles?.size){ + } else if (1 == virtualFiles?.size) { UITools.getSelectedFile(event)?.parent?.toNioPath() } else { getModuleRootForFile(UITools.getSelectedFile(event)?.parent?.toFile ?: throw RuntimeException("")).toPath() @@ -76,16 +75,17 @@ class CreateImageAction : BaseAction() { DataStorage.sessionPaths[session] = root?.toFile()!! } - agents[session] = PatchApp(event, root!!.toFile(), ::codeSummary) + SessionProxyServer.chats[session] = PatchApp(event, root!!.toFile(), ::codeSummary) val server = AppServer.getServer(event.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + 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) } @@ -237,21 +237,5 @@ class CreateImageAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(CreateImageAction::class.java) - private val agents = mutableMapOf() - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "Multi-file Patch Chat", - path = path, - showMenubar = false, - ) { - override val singleInput = true - override val stickyInput = false - override fun newSession(user: User?, session: Session) = agents[session]!!.newSession(user, session) - } - server.addApp(path, socketServer) - return socketServer - } - } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt index acbb3f2e..93e597fa 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt @@ -2,6 +2,7 @@ import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.AppServer +import com.github.simiacryptus.aicoder.actions.BaseAction.Companion import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.chatModel import com.github.simiacryptus.aicoder.util.CodeChatSocketManager @@ -14,19 +15,13 @@ import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.util.TextRange import com.simiacryptus.skyenet.core.platform.ApplicationServices -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.chat.ChatServer import com.simiacryptus.skyenet.webui.session.SessionTask -import com.simiacryptus.skyenet.webui.session.SocketManager import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.intellij.lang.annotations.Language import org.slf4j.LoggerFactory import java.awt.Desktop -import java.io.File class DiffChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -53,14 +48,14 @@ class DiffChatAction : BaseAction() { selectionStart = 0 selectionEnd = rawText.length } - agents[session] = object : CodeChatSocketManager( + SessionProxyServer.agents[session] = object : CodeChatSocketManager( session = session, language = language, codeSelection = rawText, filename = filename, api = api, model = AppSettingsState.instance.smartModel.chatModel(), - storage = ApplicationServices.dataStorageFactory(root) + storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome) ) { override val systemPrompt: String @Language("Markdown") @@ -117,21 +112,23 @@ class DiffChatAction : BaseAction() { selectionEnd = selectionStart + newCode.length document.replaceString(selectionStart, selectionStart + rawText.length, newCode) } - }, - task = task, + }, + task = task, ui = ui ) - )}""" + ) + }""" } val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + 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) } @@ -142,22 +139,5 @@ class DiffChatAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(DiffChatAction::class.java) - private val agents = mutableMapOf() - val root: File get() = File(AppSettingsState.instance.pluginHome, "code_chat") - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "Patch Chat", - path = path, - showMenubar = false, - ) { - override val singleInput = false - override val stickyInput = true - override fun newSession(user: User?, session: Session) = agents[session]!! - } - server.addApp(path, socketServer) - return socketServer - } - } } \ 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 32dbe825..fb48ee4a 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 @@ -226,7 +226,7 @@ class GenerateDocumentationAction : FileContextAction CheckBoxList.items: List +val CheckBoxList.items: List get() { val items = mutableListOf() for (i in 0 until model.size) { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenericChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenericChatAction.kt index 5297f8df..83d41cd3 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenericChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenericChatAction.kt @@ -2,25 +2,17 @@ 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.actions.BaseAction.Companion import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.chatModel -import com.github.simiacryptus.aicoder.util.CodeChatSocketManager -import com.github.simiacryptus.aicoder.util.ComputerLanguage import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.fileEditor.FileDocumentManager import com.simiacryptus.skyenet.core.platform.ApplicationServices -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.ApplicationServer -import com.simiacryptus.skyenet.webui.chat.ChatServer import com.simiacryptus.skyenet.webui.chat.ChatSocketManager -import com.simiacryptus.skyenet.webui.session.SocketManager import org.slf4j.LoggerFactory import java.awt.Desktop -import java.io.File class GenericChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -32,25 +24,26 @@ class GenericChatAction : BaseAction() { override fun handle(e: AnActionEvent) { val session = StorageInterface.newGlobalID() - agents[session] = ChatSocketManager( + SessionProxyServer.agents[session] = ChatSocketManager( session = session, model = model, initialAssistantPrompt = "", userInterfacePrompt = userInterfacePrompt, systemPrompt = systemPrompt, api = api, - storage = ApplicationServices.dataStorageFactory(root), + storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome), applicationClass = ApplicationServer::class.java, ) val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + 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) } @@ -61,23 +54,5 @@ class GenericChatAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(CodeChatAction::class.java) - private val agents = mutableMapOf() - val root: File get() = File(AppSettingsState.instance.pluginHome, "code_chat") - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "AI Chat", - path = path, - showMenubar = false, - ) { - override val singleInput = false - override val stickyInput = true - override fun newSession(user: User?, session: Session) = - agents[session] ?: throw IllegalArgumentException("Unknown session: $session") - } - server.addApp(path, socketServer) - return socketServer - } - } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MassPatchAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MassPatchAction.kt new file mode 100644 index 00000000..a8af92b5 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MassPatchAction.kt @@ -0,0 +1,296 @@ +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.actions.generic.MassPatchAction.Settings +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.chatModel +import com.github.simiacryptus.aicoder.config.Name +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.CheckBoxList +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.JBTextArea +import com.simiacryptus.diff.addApplyFileDiffLinks +import com.simiacryptus.diff.addSaveLinks +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.OpenAIClient +import com.simiacryptus.jopenai.util.ClientUtil.toContentList +import com.simiacryptus.skyenet.Discussable +import com.simiacryptus.skyenet.TabbedDisplay +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.StorageInterface +import com.simiacryptus.skyenet.core.platform.User +import com.simiacryptus.skyenet.webui.application.ApplicationServer +import com.simiacryptus.skyenet.webui.application.ApplicationSocketManager +import com.simiacryptus.skyenet.webui.session.SocketManager +import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown +import java.awt.BorderLayout +import java.awt.Desktop +import java.awt.Dimension +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.Semaphore +import java.util.concurrent.atomic.AtomicReference +import javax.swing.BoxLayout +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel + +class MassPatchAction : BaseAction() { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + override fun isEnabled(event: AnActionEvent): Boolean { + if (UITools.getSelectedFile(event)?.isDirectory == false) return false + return super.isEnabled(event) + } + + class SettingsUI { + @Name("Files to Process") + val filesToProcess = CheckBoxList() + + @Name("AI Instruction") + val transformationMessage = JBTextArea(4, 40) + + } + + class UserSettings( + var transformationMessage: String = "Create user documentation", + var filesToProcess: List = listOf(), + ) + + class Settings( + val settings: UserSettings? = null, + val project: Project? = null + ) + + /*override*/ fun getConfig(project: Project?, e: AnActionEvent): Settings { + val root = UITools.getSelectedFolder(e)?.toNioPath() + val files = Files.walk(root) + .filter { Files.isRegularFile(it) && !Files.isDirectory(it) } + .toList().filterNotNull().toTypedArray() + val settingsUI = SettingsUI().apply { + filesToProcess.setItems(files.toMutableList()) { path -> + root?.relativize(path)?.toString() ?: path.toString() + } + files.forEach { path -> + filesToProcess.setItemSelected(path, true) + } + } + val dialog = ConfigDialog(project, settingsUI) + dialog.show() + val result = dialog.isOK + val settings: UserSettings = dialog.userSettings + settings.filesToProcess = when { + result -> files.filter { path -> settingsUI.filesToProcess.isItemSelected(path) }.toList() + else -> listOf() + } + return Settings(settings, project) + } + + override fun handle(e: AnActionEvent) { + val project = e.project + val config = getConfig(project, e) + + val codeSummary = config?.settings?.filesToProcess?.filter { + it.toFile().exists() + }?.associateWith { it.toFile().readText(Charsets.UTF_8) } + ?.entries?.joinToString("\n\n") { (path, code) -> + val extension = path.toString().split('.').lastOrNull() + """ + |# $path + |```$extension + |${code.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} + |``` + """.trimMargin() + } + + + val session = StorageInterface.newGlobalID() + SessionProxyServer.chats[session] = MassPatchServer(codeSummary=codeSummary, config=config, api=api) + + val server = AppServer.getServer(e.project) + Thread { + Thread.sleep(500) + try { + val uri = server.server.uri.resolve("/#$session") + log.info("Opening browser to $uri") + Desktop.getDesktop().browse(uri) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + + } + + class ConfigDialog(project: Project?, private val settingsUI: SettingsUI) : DialogWrapper(project) { + val userSettings = UserSettings() + + init { + title = "Compile Documentation" + // Set the default values for the UI elements from userSettings + settingsUI.transformationMessage.text = userSettings.transformationMessage + 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("AI Instruction")) + add(settingsUI.transformationMessage) + } + add(optionsPanel, BorderLayout.SOUTH) + } + return panel + } + + override fun doOKAction() { + super.doOKAction() + userSettings.transformationMessage = settingsUI.transformationMessage.text + userSettings.filesToProcess = + settingsUI.filesToProcess.items.filter { path -> settingsUI.filesToProcess.isItemSelected(path) } + } + } +} + +class MassPatchServer( + val codeSummary: String?, + val config: Settings, + val api: OpenAIClient +) : ApplicationServer( + applicationName = "Multi-file Patch Chat", + path = "/patchChat", + showMenubar = false, +) { + override val singleInput = false + override val stickyInput = true + private val mainActor: SimpleActor + get() = 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 ```diff code blocks. + |Each diff should be preceded by a header that identifies the file being modified. + |The diff format should use + for line additions, - for line deletions. + |The diff should include 2 lines of context before and after every change. + | + |Example: + | + |Here are the patches: + | + |### src/utils/exampleUtils.js + |```diff + | // Utility functions for example feature + | const b = 2; + | function exampleFunction() { + |- return b + 1; + |+ return b + 2; + | } + |``` + | + |### tests/exampleUtils.test.js + |```diff + | // Unit tests for exampleUtils + | const assert = require('assert'); + | const { exampleFunction } = require('../src/utils/exampleUtils'); + | + | describe('exampleFunction', () => { + |- it('should return 3', () => { + |+ it('should return 4', () => { + | assert.equal(exampleFunction(), 3); + | }); + | }); + |``` + | + |If needed, new files can be created by using code blocks labeled with the filename in the same manner. + """.trimMargin(), + model = AppSettingsState.instance.smartModel.chatModel(), + temperature = AppSettingsState.instance.temperature, + ) + + override fun newSession(user: User?, session: Session): SocketManager { + val socketManager = super.newSession(user, session) + val ui = (socketManager as ApplicationSocketManager).applicationInterface + val task = ui.newTask(true) + val tabs = TabbedDisplay(task) + val userMessage = config.settings?.transformationMessage ?: "Create user documentation" + val codeFiles = config.settings?.filesToProcess + codeFiles?.forEach { path -> + socketManager.scheduledThreadPoolExecutor.schedule({ + socketManager.pool.submit { + try { + val fileTask = ui.newTask(false).apply { + tabs[path.toString()] = placeholder + } + val toInput = { it: String -> listOf(codeSummary ?: "", it) } + Discussable( + task = fileTask, + userMessage = { userMessage }, + heading = renderMarkdown(userMessage), + initialResponse = { + mainActor.answer(toInput(it), api = api) + }, + outputFn = { design: String -> + var markdown = ui.socketManager?.addApplyFileDiffLinks( + root = root.toPath(), + code = { codeFiles.associateWith { root.resolve(it.toFile()).readText(Charsets.UTF_8) } }, + response = design, + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + fileTask.complete("$path Updated") + } + }, + ui = ui, + ) + markdown = ui.socketManager?.addSaveLinks( + response = markdown!!, + task = fileTask, + ui = ui, + handle = { path, newCode -> + root.resolve(path.toFile()).writeText(newCode, Charsets.UTF_8) + }, + ) + """
${renderMarkdown(markdown!!)}
""" + }, + ui = ui, + reviseResponse = { userMessages: List> -> + mainActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput(userMessage), + api = api + ) + }, + atomicRef = AtomicReference(), + semaphore = Semaphore(0), + ).call() + } catch (e: Exception) { + log.warn("Error processing $path", e) + task.error(ui, e) + } + } + }, 10, java.util.concurrent.TimeUnit.MILLISECONDS) + + } + return socketManager + } + + companion object { + val log = org.slf4j.LoggerFactory.getLogger(MassPatchServer::class.java) + } +} 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 0fb7d2e8..14873dc9 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 @@ -2,6 +2,7 @@ import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.actions.BaseAction.Companion import com.github.simiacryptus.aicoder.actions.generic.MultiStepPatchAction.AutoDevApp.Settings import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools @@ -24,7 +25,6 @@ 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.chat.ChatServer import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory import java.awt.Desktop @@ -36,8 +36,6 @@ import java.util.concurrent.atomic.AtomicReference class MultiDiffChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT - val path = "/multiDiffChat" - override fun handle(event: AnActionEvent) { var root: Path? = null val codeFiles: MutableSet = mutableSetOf() @@ -62,19 +60,20 @@ class MultiDiffChatAction : BaseAction() { } else { getModuleRootForFile(UITools.getSelectedFile(event)?.parent?.toFile ?: throw RuntimeException("")).toPath() } - val files = getFiles(virtualFiles, root!!) codeFiles.addAll(files) + val session = StorageInterface.newGlobalID() - agents[session] = PatchApp(event, root!!.toFile(), { codeSummary() }, codeFiles) + SessionProxyServer.chats[session] = PatchApp(root!!.toFile(), { codeSummary() }, codeFiles) val server = AppServer.getServer(event.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + 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) } @@ -82,13 +81,12 @@ class MultiDiffChatAction : BaseAction() { } inner class PatchApp( - private val event: AnActionEvent, override val root: File, val codeSummary: () -> String, val codeFiles: Set = setOf(), ) : ApplicationServer( applicationName = "Multi-file Patch Chat", - path = path, + path = "/patchChat", showMenubar = false, ) { override val singleInput = false @@ -162,7 +160,7 @@ class MultiDiffChatAction : BaseAction() { heading = renderMarkdown(userMessage), initialResponse = { it: String -> mainActor.answer(toInput(it), api = api) }, outputFn = { design: String -> - var markdown = ui.socketManager.addApplyFileDiffLinks( + var markdown = ui.socketManager?.addApplyFileDiffLinks( root = root.toPath(), code = { codeFiles.associateWith { root.resolve(it.toFile()).readText(Charsets.UTF_8) } }, response = design, @@ -173,15 +171,15 @@ class MultiDiffChatAction : BaseAction() { }, ui = ui, ) - markdown = ui.socketManager.addSaveLinks( - response = markdown, + markdown = ui.socketManager?.addSaveLinks( + response = markdown!!, task = task, ui = ui, handle = { path, newCode -> root.resolve(path.toFile()).writeText(newCode, Charsets.UTF_8) }, ) - """
${renderMarkdown(markdown)}
""" + """
${renderMarkdown(markdown!!)}
""" }, ui = ui, reviseResponse = { userMessages: List> -> @@ -218,21 +216,7 @@ class MultiDiffChatAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(MultiDiffChatAction::class.java) - private val agents = mutableMapOf() - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "Multi-file Patch Chat", - path = path, - showMenubar = false, - ) { - override val singleInput = true - override val stickyInput = false - override fun newSession(user: User?, session: Session) = agents[session]!!.newSession(user, session) - } - server.addApp(path, socketServer) - return socketServer - } } } + diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt index 87154ff2..dc64b9df 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt @@ -1,14 +1,15 @@ package com.github.simiacryptus.aicoder.actions.generic import ai.grazie.utils.mpp.UUID -import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.AppServer +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.actions.BaseAction.Companion import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools -import com.simiacryptus.diff.addApplyFileDiffLinks import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.simiacryptus.diff.addApplyFileDiffLinks import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.ApiModel.Role @@ -17,8 +18,8 @@ import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.proxy.ValidatedObject import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.jopenai.util.JsonUtil.toJson -import com.simiacryptus.skyenet.Discussable import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.Discussable import com.simiacryptus.skyenet.Retryable import com.simiacryptus.skyenet.TabbedDisplay import com.simiacryptus.skyenet.core.actors.* @@ -26,7 +27,6 @@ import com.simiacryptus.skyenet.core.platform.* 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.chat.ChatServer import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory import java.awt.Desktop @@ -42,19 +42,21 @@ class MultiStepPatchAction : BaseAction() { override fun handle(e: AnActionEvent) { val session = StorageInterface.newGlobalID() - val storage = ApplicationServices.dataStorageFactory(DiffChatAction.root) as DataStorage? + val storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome) as DataStorage? val selectedFile = UITools.getSelectedFolder(e) if (null != storage && null != selectedFile) { DataStorage.sessionPaths[session] = selectedFile.toFile } - agents[session] = AutoDevApp(event = e) + SessionProxyServer.chats[session] = AutoDevApp(event = e) val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) + Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + 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) } @@ -254,7 +256,7 @@ class MultiStepPatchAction : BaseAction() { | """.trimMargin() } - renderMarkdown(ui.socketManager.addApplyFileDiffLinks( + renderMarkdown(ui.socketManager!!.addApplyFileDiffLinks( root = root, code = { codeFiles.associateWith { root.resolve(it).toFile().readText() } }, response = taskActor.answer(listOf( @@ -289,18 +291,7 @@ class MultiStepPatchAction : BaseAction() { } companion object { private val log = LoggerFactory.getLogger(MultiStepPatchAction::class.java) - private val agents = mutableMapOf() val root: File get() = File(AppSettingsState.instance.pluginHome, "code_chat") - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer(applicationName = "Multi-Patch Chat", path = path) { - override val singleInput = true - override val stickyInput = false - override fun newSession(user: User?, session: Session) = agents[session]!!.newSession(user, session) - } - server.addApp(path, socketServer) - return socketServer - } data class TaskList( @Description("List of tasks to be performed in this project") 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 30ddef45..06b8cb0d 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 @@ -1,12 +1,10 @@ package com.github.simiacryptus.aicoder.actions.generic -import com.github.simiacryptus.aicoder.actions.BaseAction 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.UITools -import com.simiacryptus.diff.addApplyFileDiffLinks -import com.simiacryptus.diff.addSaveLinks import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys.VIRTUAL_FILE_ARRAY @@ -15,14 +13,16 @@ 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.addApplyFileDiffLinks +import com.simiacryptus.diff.addSaveLinks import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.ApiModel.Role import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.util.ClientUtil.toContentList import com.simiacryptus.jopenai.util.JsonUtil.toJson -import com.simiacryptus.skyenet.Discussable 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 @@ -37,7 +37,6 @@ 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.chat.ChatServer import com.simiacryptus.skyenet.webui.session.SessionTask import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory @@ -121,13 +120,14 @@ class PlanAheadAction : BaseAction() { // Settings are applied only if the user clicks OK val session = StorageInterface.newGlobalID() val folder = UITools.getSelectedFolder(e) - val root = folder?.toFile ?: getModuleRootForFile(UITools.getSelectedFile(e)?.parent?.toFile ?: throw RuntimeException("")) + val root = folder?.toFile ?: getModuleRootForFile( + UITools.getSelectedFile(e)?.parent?.toFile ?: throw RuntimeException("") + ) DataStorage.sessionPaths[session] = root - PlanAheadApp.agents[session] = PlanAheadApp(event = e, root = root, settings = settings) + SessionProxyServer.chats[session] = PlanAheadApp(event = e, root = root, settings = settings) val server = AppServer.getServer(project) - val app = PlanAheadApp.initApp(server, path) - app.sessions[session] = app.newSession(null, session) + openBrowser(server, session.toString()) } } @@ -136,7 +136,10 @@ class PlanAheadAction : BaseAction() { Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + val uri = server.server.uri.resolve("/#$session") + log.info("Opening browser to $uri") + Desktop.getDesktop().browse(uri) } catch (e: Throwable) { LoggerFactory.getLogger(PlanAheadAction::class.java).warn("Error opening browser", e) } @@ -206,22 +209,6 @@ class PlanAheadApp( } companion object { - fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "Task Planning Agent", - path = path, - showMenubar = false, - ) { - override val singleInput = true - override val stickyInput = false - override fun newSession(user: User?, session: Session) = agents[session]!!.newSession(user, session) - } - server.addApp(path, socketServer) - return socketServer - } - - val agents = mutableMapOf() private val log = LoggerFactory.getLogger(PlanAheadApp::class.java) } } @@ -799,7 +786,7 @@ class PlanAheadAgent( ).filter { it.isNotBlank() }, api ) genState.taskResult[taskId] = codeResult - renderMarkdown(ui.socketManager.addSaveLinks(codeResult, task, ui = ui) { path, newCode -> + renderMarkdown(ui.socketManager!!.addSaveLinks(codeResult, task, ui = ui) { path, newCode -> val prev = codeFiles[path] if (prev != newCode) { val bytes = newCode.toByteArray(Charsets.UTF_8) @@ -845,7 +832,7 @@ class PlanAheadAgent( ) genState.taskResult[taskId] = codeResult renderMarkdown( - ui.socketManager.addApplyFileDiffLinks( + ui.socketManager!!.addApplyFileDiffLinks( root = root, code = { codeFiles }, response = codeResult, diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SessionProxyApp.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SessionProxyApp.kt new file mode 100644 index 00000000..8650a7db --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SessionProxyApp.kt @@ -0,0 +1,25 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.User +import com.simiacryptus.skyenet.webui.application.ApplicationServer +import com.simiacryptus.skyenet.webui.chat.ChatServer +import com.simiacryptus.skyenet.webui.session.SocketManager + +class SessionProxyServer : ApplicationServer( + applicationName = "AI Coding Assistant", + path = "/", + showMenubar = false, +) { + override val singleInput = true + override val stickyInput = false + override fun newSession(user: User?, session: Session) = + chats[session]?.newSession(user, session) ?: agents[session]!! + + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(SessionProxyServer::class.java) + + val agents = mutableMapOf() + val chats = mutableMapOf() + } +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt index 978e9ac0..8a304992 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt @@ -2,9 +2,9 @@ import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.actions.BaseAction.Companion import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools -import com.simiacryptus.diff.addApplyFileDiffLinks import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.vfs.VirtualFile @@ -22,12 +22,13 @@ 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.* +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.chat.ChatServer -//import com.simiacryptus.skyenet.webui.servlet.ToolServlet import com.simiacryptus.skyenet.webui.session.SessionTask import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown import org.slf4j.LoggerFactory @@ -54,14 +55,16 @@ class WebDevelopmentAssistantAction : BaseAction() { if (/*null != storage &&*/ null != selectedFile) { DataStorage.sessionPaths[session] = selectedFile.toFile } - agents[session] = WebDevApp(root = selectedFile) + SessionProxyServer.chats[session] = WebDevApp(root = selectedFile) val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) + Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + + 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) } @@ -415,7 +418,7 @@ class WebDevelopmentAssistantAction : BaseAction() { }, outputFn = { code -> renderMarkdown( - ui.socketManager.addApplyFileDiffLinks( + ui.socketManager!!.addApplyFileDiffLinks( root = root.toPath(), code = { codeFiles.filter { if (it.name.lowercase().endsWith(".png")) return@filter false @@ -613,23 +616,7 @@ class WebDevelopmentAssistantAction : BaseAction() { companion object { private val log = LoggerFactory.getLogger(WebDevelopmentAssistantAction::class.java) - private val agents = mutableMapOf() val root: File get() = File(AppSettingsState.instance.pluginHome, "code_chat") - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer( - applicationName = "Web Development Agent", - path = path, - showMenubar = false, - ) { - override val singleInput = true - override val stickyInput = false - override fun newSession(user: User?, session: Session) = agents[session]!!.newSession(user, session) - } - server.addApp(path, socketServer) - return socketServer - } - data class ProjectSpec( @Description("Files in the project design, including all local html, css, and js files.") val files: List = emptyList() 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 0a284919..0ee175af 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -1,6 +1,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.FileChooserDescriptorFactory @@ -21,7 +22,6 @@ import java.awt.event.ActionEvent import java.io.FileOutputStream import javax.swing.AbstractAction import javax.swing.JButton -import javax.swing.JLabel import javax.swing.JList import javax.swing.ListCellRenderer import javax.swing.table.DefaultTableCellRenderer @@ -156,7 +156,10 @@ class AppSettingsComponent : com.intellij.openapi.Disposable { init { ChatModels.values() - .filter { AppSettingsState.instance.apiKey?.filter { it.value.isNotBlank() }?.keys?.contains(it.value.provider.name) ?: false } + .filter { + AppSettingsState.instance.apiKey?.filter { it.value.isNotBlank() }?.keys?.contains(it.value.provider.name) + ?: false + } .forEach { this.smartModel.addItem(it.value.modelName) this.fastModel.addItem(it.value.modelName) @@ -165,12 +168,24 @@ class AppSettingsComponent : com.intellij.openapi.Disposable { this.mainImageModel.addItem(it.name) } // Sort the items in the ComboBoxes - val smartModelItems = (0 until smartModel.itemCount).map { smartModel.getItemAt(it) }.sortedBy { modelItem -> - val model = ChatModels.values().entries.find { it.value.modelName == modelItem }?.value ?: return@sortedBy "" - "${model.provider.name} - ${model.modelName}" }.toList() - val fastModelItems = (0 until fastModel.itemCount).map { fastModel.getItemAt(it) }.sortedBy { modelItem -> - val model = ChatModels.values().entries.find { it.value.modelName == modelItem }?.value ?: return@sortedBy "" - "${model.provider.name} - ${model.modelName}" }.toList() + val smartModelItems = (0 until smartModel.itemCount).map { smartModel.getItemAt(it) } + .filter { modelItem -> + isVisible(ChatModels.values().entries.find { it.value.modelName == modelItem }?.value ?: return@filter false) + } + .sortedBy { modelItem -> + val model = + ChatModels.values().entries.find { it.value.modelName == modelItem }?.value ?: return@sortedBy "" + "${model.provider.name} - ${model.modelName}" + }.toList() + val fastModelItems = (0 until fastModel.itemCount).map { fastModel.getItemAt(it) } + .filter { modelItem -> + isVisible(ChatModels.values().entries.find { it.value.modelName == modelItem }?.value ?: return@filter false) + } + .sortedBy { modelItem -> + val model = + ChatModels.values().entries.find { it.value.modelName == modelItem }?.value ?: return@sortedBy "" + "${model.provider.name} - ${model.modelName}" + }.toList() smartModel.removeAllItems() fastModel.removeAllItems() smartModelItems.forEach { smartModel.addItem(it) } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt index 944da1ce..a865cfec 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt @@ -10,9 +10,19 @@ import com.intellij.openapi.wm.StatusBarWidgetFactory import com.intellij.ui.CollectionListModel import com.intellij.ui.SimpleListCellRenderer import com.intellij.ui.components.JBList +import com.simiacryptus.jopenai.OpenAIClient.AWSAuth +import com.simiacryptus.jopenai.models.APIProvider import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.util.JsonUtil import icons.MyIcons import kotlinx.coroutines.CoroutineScope +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.bedrock.BedrockClient +import software.amazon.awssdk.services.bedrock.model.FoundationModelLifecycleStatus +import software.amazon.awssdk.services.bedrock.model.GetFoundationModelRequest +import software.amazon.awssdk.services.bedrock.model.GetProvisionedModelThroughputRequest +import software.amazon.awssdk.services.bedrock.model.ListFoundationModelsRequest import java.awt.BorderLayout import java.awt.Cursor import java.awt.Desktop @@ -44,10 +54,8 @@ class SettingsWidgetFactory : StatusBarWidgetFactory { } } - fun models() = ChatModels.values().filter { - AppSettingsState.instance.apiKey?.filter { it.value.isNotBlank() }?.keys?.contains(it.value.provider.name) - ?: false - }.entries.sortedBy { "${it.value.provider.name} - ${it.value.modelName}" }.map { it.value }.toList() + fun models() = ChatModels.values().filter { isVisible(it.value) } + .entries.sortedBy { "${it.value.provider.name} - ${it.value.modelName}" }.map { it.value }.toList() override fun ID(): String { return "AICodingAssistant.SettingsWidget" @@ -131,6 +139,31 @@ class SettingsWidgetFactory : StatusBarWidgetFactory { return popup } + + companion object { + fun isVisible(it: ChatModels): Boolean { + val hasApiKey = + AppSettingsState.instance.apiKey?.filter { it.value.isNotBlank() }?.keys?.contains(it.provider.name) + if(false == hasApiKey) return false +/* + if (it.provider == APIProvider.AWS) { + val modelName = it.modelName + val awsAuth = JsonUtil.fromJson(AppSettingsState.instance.apiKey?.get(it.provider.name)!!, AWSAuth::class.java) + val bedrockClient = BedrockClient.builder() + .credentialsProvider(ProfileCredentialsProvider.builder().profileName(awsAuth.profile).build()) + .region(Region.of(awsAuth.region)) + .build() + val listFoundationModels = + bedrockClient.listFoundationModels(ListFoundationModelsRequest.builder().build()) + val foundationModel = bedrockClient.getFoundationModel( + GetFoundationModelRequest.builder().modelIdentifier(modelName).build() + ) + foundationModel.modelDetails().modelLifecycle().status() == FoundationModelLifecycleStatus.ACTIVE + } +*/ + return true + } + } } override fun getId(): String { 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 1e3c63d5..f8cc0371 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt @@ -33,7 +33,7 @@ class IdeaOpenAIClient : OpenAIClient( ) { init { - log.info("Initializing OpenAI Client", Throwable()) + //log.info("Initializing OpenAI Client", Throwable()) require(key.size == apiBase.size) { "API Key not configured for all providers: ${key.keys} != ${APIProvider.values().toList()}" } @@ -162,7 +162,7 @@ class IdeaOpenAIClient : OpenAIClient( companion object { val instance by lazy { - log.info("Initializing OpenAI Client", Throwable()) + //log.info("Initializing OpenAI Client", Throwable()) val client = IdeaOpenAIClient() if (AppSettingsState.instance.apiLog) { try { 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 647c1e64..63638ce0 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/PluginStartupActivity.kt @@ -102,8 +102,7 @@ class PluginStartupActivity : ProjectActivity { override fun logout(accessToken: String, user: User) {} } ApplicationServices.isLocked = true - val resolve = AppSettingsState.instance.pluginHome.resolve(".sys") - DataStorage.SYS_DIR = resolve.apply { mkdirs() } + val resolve = AppSettingsState.instance.pluginHome.resolve(".skyenet") } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a30cf23e..1bf62868 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -101,6 +101,12 @@ + + + +