diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba40d8a..06981b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,33 @@ ## [Unreleased] +## [1.4.0] + +### Added + +- Support for Gemini models +- Support for Mistral Large + +### Removed + +- Dynamic action support (temporarily) due to performance issues and bugs + +### Improved + +- Patching logic +- Various fixes + ## [1.3.0] ### Added -- `DiffChatAction`: A new action for engaging in a chat session to generate and apply code diffs directly within the IDE. +- `DiffChatAction`: A new action for engaging in a chat session to generate and apply code diffs directly within the + IDE. - `MultiDiffChatAction`: Allows for collaborative code review and diff generation across multiple files. -- `AutoDevAction`: Automates development tasks by translating user directives into actionable development tasks and code modifications. -- Support for models from https://www.perplexity.ai/, https://console.groq.com/, and https://modelslab.com/dashboard/, enhancing the plugin's versatility and performance in code generation and analysis. +- `AutoDevAction`: Automates development tasks by translating user directives into actionable development tasks and code + modifications. +- Support for models from https://www.perplexity.ai/, https://console.groq.com/, and https://modelslab.com/dashboard/, + enhancing the plugin's versatility and performance in code generation and analysis. ### Improved @@ -437,49 +456,95 @@ from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template) [Unreleased]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.18...HEAD + [1.2.18]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.14...v1.2.18 + [1.2.14]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.11...v1.2.14 + [1.2.11]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.10...v1.2.11 + [1.2.10]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.9...v1.2.10 + [1.2.9]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.8...v1.2.9 + [1.2.8]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.7...v1.2.8 + [1.2.7]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.4...v1.2.7 + [1.2.4]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.2...v1.2.4 + [1.2.2]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.1...v1.2.2 + [1.2.1]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.2.0...v1.2.1 + [1.2.0]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.1.4...v1.2.0 + [1.1.4]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.1.3...v1.1.4 + [1.1.3]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.1.2...v1.1.3 + [1.1.2]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.1.1...v1.1.2 + [1.1.1]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.1.0...v1.1.1 + [1.1.0]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.20...v1.1.0 + [1.0.20]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.19...v1.0.20 + [1.0.19]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.18...v1.0.19 + [1.0.18]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.17...v1.0.18 + [1.0.17]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.16...v1.0.17 + [1.0.16]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.15...v1.0.16 + [1.0.15]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.14...v1.0.15 + [1.0.14]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.13...v1.0.14 + [1.0.13]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.12...v1.0.13 + [1.0.12]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.11...v1.0.12 + [1.0.11]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.10...v1.0.11 + [1.0.10]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.9...v1.0.10 + [1.0.9]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.8...v1.0.9 + [1.0.8]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.7...v1.0.8 + [1.0.7]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.6...v1.0.7 + [1.0.6]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.5...v1.0.6 + [1.0.5]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.3...v1.0.5 + [1.0.3]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.2...v1.0.3 + [1.0.2]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v1.0.0...v1.0.2 + [1.0.0]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.9...v1.0.0 + [0.1.9]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.8...v0.1.9 + [0.1.8]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.7...v0.1.8 + [0.1.7]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.6...v0.1.7 + [0.1.6]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.5...v0.1.6 + [0.1.5]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.4...v0.1.5 + [0.1.4]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.3...v0.1.4 + [0.1.3]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.2...v0.1.3 + [0.1.2]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.1.0...v0.1.2 + [0.1.0]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.0.4...v0.1.0 + [0.0.4]: https://github.com/SimiaCryptus/intellij-aicoder/compare/v0.0.1...v0.0.4 -[0.0.1]: https://github.com/SimiaCryptus/intellij-aicoder/commits/v0.0.1 + +[0.0.1]: https://github.com/SimiaCryptus/intellij-aicoder/commits/v0.0.1 \ No newline at end of file diff --git a/README.md b/README.md index 06b8afb8..86b784b1 100644 --- a/README.md +++ b/README.md @@ -6,90 +6,73 @@ -# **AI Coding Assistant Plugin for IntelliJ** +# **AI Coding Assistant: The Ultimate IntelliJ Plugin for Developers** -**Fully open source plugin for IntelliJ that integrates with a variety of LLM APIs** - -* No membership fees! API access is typically paid but is billed by usage, with no base fee. -* The plugin is free and open source, with a focus on utility. No hype here! I'm sharing a tool I use, and I hope you find it useful too. -* A variety of APIs are supported! Get your api keys at: - * https://platform.openai.com/ - * https://www.perplexity.ai/ - * https://console.groq.com/ - * https://modelslab.com/dashboard/ - * https://console.aws.amazon.com/bedrock/ -* Fully customizable actions, including the ability to create your own actions using AI -* Toolbar UI for quick configuration of temperature/model and display of current token count -* Ability to intercept, edit, and log API requests - -**NOTE**: This project is not affiliated with OpenAI, JetBrains, or any other corporation or organization. -It is provided free of charge, as-is, with no warranty or guarantee of any kind. -It is the work of a sole developer working as a hobby project. - -## **Installation & Configuration** - -To begin with AI Coding Assistant, you will need one or more api tokens, -which needs to be input it into the appropriate field in the plugin's settings panel. +![Build](https://github.com/SimiaCryptus/intellij-aicoder/workflows/Build/badge.svg) +[![Version](https://img.shields.io/jetbrains/plugin/v/20724-ai-coding-assistant.svg)](https://plugins.jetbrains.com/plugin/20724-ai-coding-assistant) +[![Downloads](https://img.shields.io/jetbrains/plugin/d/20724-ai-coding-assistant.svg)](https://plugins.jetbrains.com/plugin/20724-ai-coding-assistant) -## **Usage Overview** +Are you a developer looking to supercharge your coding workflow? Look no further than AI Coding Assistant, the +game-changing IntelliJ plugin that harnesses the power of cutting-edge Language Model APIs to revolutionize the way you +code! -AI Coding Assistant offers a variety of actions, which are tools specifically designed to simplify and speed up your -coding process. These are not passively triggered by your typing but are invoked on command, giving you full control -over when and how to use them. You can access these actions via the context menu within an editor or in the project view. +## **Key Features** -### **New in Version 1.3.0** +* 🌐 **Extensive API Support**: AI Coding Assistant seamlessly integrates with a wide range of leading LLM APIs, + including [OpenAI](https://platform.openai.com/), [AWS Bedrock](https://console.aws.amazon.com/bedrock/), [Anthropic](https://api.anthropic.com/v1), [Google Generative Language](https://generativelanguage.googleapis.com/), [Groq](https://console.groq.com/), [Perplexity AI](https://www.perplexity.ai/), + and [ModelsLab](https://modelslab.com/dashboard/). Get your API keys and unlock a world of possibilities! -* **DiffChatAction**: Engage in a chat session to generate and apply code diffs directly within the IDE, streamlining the code review and modification process. -* **MultiDiffChatAction**: Facilitates collaborative code review and diff generation across multiple files, enhancing team productivity. -* **AutoDevAction**: Translates user directives into actionable development tasks and code modifications, automating parts of the development workflow. -* **mermaid.js**: Generate diagrams using the mermaid.js library, providing a visual representation of your code and ideas. +* 💸 **No Membership Fees**: While API access is typically paid, AI Coding Assistant charges no base fee. You only pay + for what you use, giving you complete control over your expenses. -## **Action Customization** +* 🌟 **Free and Open Source**: AI Coding Assistant is a free, open-source tool designed with a focus on utility. No hype, + no gimmicks – just a powerful, user-friendly plugin that enhances your coding experience. -Tailor actions to your coding habits and project requirements! -Within the settings UI, you can view, edit, clone, or delete actions, -enabling you to fine-tune existing tools or create new ones from scratch. +* 🎛️ **Intuitive Toolbar UI**: Easily configure temperature/model settings and monitor your current token count with AI + Coding Assistant's sleek, intuitive toolbar UI. -These custom actions use dynamically compiled Kotlin, and can be as complex or simple as required, even having access to the -entire IntelliJ API. This powerful feature allows for numerous possibilities, from refining prompts to adding intricate -logic to better support your preferred coding language. +* 🔍 **API Request Management**: Intercept, edit, and log API requests with ease, giving you granular control over your + plugin's behavior. -## **Actions Catalogue** +## **Installation & Setup** -Our plugin includes a broad catalogue of actions, categorized into Plaintext, Code, Markdown, and Developer-Mode -Actions. +Getting started with AI Coding Assistant is a breeze: -### **Plaintext Actions** +1. Obtain one or more API tokens from your preferred LLM API provider(s). +2. Input your token(s) into the appropriate field(s) in the plugin's settings panel. +3. Start coding smarter, not harder! -These actions offer text processing features for any language, and include tools such -as `Chat Append Text`, `Dictation`, `Redo Last`, `Replace Options`, and `Generate Story`. +## **Unleash Your Coding Potential** -### **Code Actions** +AI Coding Assistant offers a suite of powerful actions designed to streamline your coding process. Access these +game-changing features via the context menu within your editor or project view: -Our Code Actions simplify and expedite your coding workflow with actions -like `Add Code Comments`, `Convert To...`, `Edit Code...`, `Describe Code and Prepend Comment`, `Add Doc Comments`, -`Implement Stub`, `Insert Implementation`, `Paste`, `Ask a question about the code`, `Recent Code Edits`, -`Rename Variables`, and `Generate Project`. +* 💬 **DiffChatAction & MultiDiffChatAction**: Collaborate effortlessly with AI-powered chat sessions that generate and + apply code diffs across single or multiple files. -### **Markdown Actions** +* 🚀 **AutoDevAction**: Transform user directives into actionable development tasks and code modifications, automating + key aspects of your workflow. -Our Markdown Actions allow quick and easy additions to your Markdown documents. You can swiftly implement a Markdown -prompt in a specific language with the `Implement As...` action or rapidly add list items to your document using -the `Add List Items` action. +* 📊 **mermaid.js Integration**: Visualize your code and ideas with stunning diagrams generated using the mermaid.js + library. -### **Developer-Mode Actions** +* 📝 **Plaintext, Code, and Markdown Actions**: From text processing and code editing to Markdown enhancements, AI Coding + Assistant has you covered. -The Developer-Mode Actions are available when the plugin is in developer mode. These actions offer debugging and -development tools and experimental features that may not be fully functional. They -include `Open Code Chat`, `Launch Skyenet`, and `Print PSI Tree`. +* 🛠️ **Developer-Mode Actions**: Access debugging tools, experimental features, and more with our developer-mode + actions. -## **Support** +## **Join the AI Coding Revolution** -Should you encounter issues or require further information, please file an issue on our github project. +Ready to take your coding to the next level? Install AI Coding Assistant today and experience the future of software +development. With our powerful features, extensive API support, and user-friendly interface, you'll wonder how you ever +coded without it! -Understand that this plugin is in active development, and we are constantly working to improve and expand its abilities. +🌟 Boost your productivity, unleash your creativity, and code smarter with AI Coding Assistant – the ultimate IntelliJ +plugin for developers. 🌟 -With AI Coding Assistant, coding becomes more efficient, versatile, and customizable, enhancing your productivity and -creative potential. We look forward to seeing what you will create! +*Please note that AI Coding Assistant is not affiliated with OpenAI, JetBrains, or any other corporation or +organization. The plugin is provided free of charge, as-is, with no warranty or guarantee of any kind, and is the work +of a sole developer working on a hobby project.* - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8e6c4a68..ac0518d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,7 +26,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.61" +val skyenet_version = "1.0.62" val remoterobot_version = "0.11.21" dependencies { @@ -39,7 +39,7 @@ dependencies { exclude(group = "org.jetbrains.kotlin", module = "") } - implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.50") + implementation(group = "com.simiacryptus", name = "jo-penai", version = "1.0.51") { exclude(group = "org.jetbrains.kotlin", module = "") } @@ -59,6 +59,7 @@ dependencies { exclude(group = "org.jetbrains.kotlin", module = "") } + implementation(group = "com.vladsch.flexmark", name = "flexmark-all", version = "0.64.8") implementation("com.googlecode.java-diff-utils:diffutils:1.3.0") implementation(group = "org.apache.httpcomponents.client5", name = "httpclient5", version = "5.2.3") implementation(group = "org.eclipse.jetty", name = "jetty-server", version = jetty_version) @@ -84,6 +85,7 @@ dependencies { } +/* tasks.register("copySourcesToResources") { from("src/main/kotlin") into("src/main/resources/sources/kt") @@ -91,6 +93,7 @@ tasks.register("copySourcesToResources") { tasks.named("processResources") { dependsOn("copySourcesToResources") } +*/ kotlin { @@ -162,7 +165,7 @@ tasks { publishPlugin { dependsOn("patchChangelog") token.set(System.getenv("PUBLISH_TOKEN")) - channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) +// channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) } } diff --git a/gradle.properties b/gradle.properties index 673c8b11..8a41d557 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.3.10 +pluginVersion=1.4.0 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g @@ -10,8 +10,8 @@ pluginSinceBuild=232 pluginUntilBuild=241.* platformType = IC -platformVersion=2023.3 -gradleVersion=8.4 +platformVersion=2024.1 +gradleVersion=8.6 # Opt-out flag for bundling Kotlin standard library -> https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library # suppress inspection "UnusedProperty" diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt deleted file mode 100644 index a292a2f0..00000000 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.dev - -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.IdeaKotlinInterpreter -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.simiacryptus.jopenai.API -import com.simiacryptus.skyenet.apps.coding.CodingAgent -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.application.ApplicationSocketManager -import org.slf4j.LoggerFactory -import java.awt.Desktop -import java.io.File - -class InternalCoderAction : BaseAction() { - - val path = "/internalCoder" - - override fun handle(e: AnActionEvent) { - val session = StorageInterface.newGlobalID() - val server = AppServer.getServer(e.project) - val codingApp = initApp(server, path) - val socketManager = codingApp.newSession(null, session) - codingApp.sessions[session] = socketManager - - val symbols: MutableMap = mapOf( - "event" to e, - ).toMutableMap() - e.getData(CommonDataKeys.EDITOR)?.apply { symbols["editor"] = this } - e.getData(CommonDataKeys.PSI_FILE)?.apply { symbols["file"] = this } - e.getData(CommonDataKeys.PSI_ELEMENT)?.apply { symbols["element"] = this } - e.getData(CommonDataKeys.VIRTUAL_FILE)?.apply { symbols["virtualFile"] = this } - e.project?.apply { symbols["project"] = this } - e.getData(CommonDataKeys.SYMBOLS)?.apply { symbols["symbols"] = this } - e.getData(CommonDataKeys.CARET)?.apply { symbols["psiElement"] = this } - e.getData(CommonDataKeys.CARET)?.apply { symbols["psiElement"] = this } - - agents[session] = CodingAgent( - api = api, - dataStorage = ApplicationServices.dataStorageFactory(codingApp.root), - session = session, - user = null, - ui = (socketManager as ApplicationSocketManager).applicationInterface, - interpreter = IdeaKotlinInterpreter::class, - symbols = symbols, - temperature = 0.1, - details = "Ensure that responses are printed; this is not a REPL.", - model = AppSettingsState.instance.smartModel.chatModel(), - ) - - Thread { - Thread.sleep(500) - try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() - } - - override fun isEnabled(event: AnActionEvent) = when { - !AppSettingsState.instance.devActions -> false - else -> true - } - - companion object { - private val log = LoggerFactory.getLogger(InternalCoderAction::class.java) - private val agents = mutableMapOf>() - private fun initApp(server: AppServer, path: String): ApplicationServer { - server.appRegistry[path]?.let { return it as ApplicationServer } - val codingApp = object : ApplicationServer( - applicationName = "IntelliJ Internal Coding Agent", - path = path, - ) { - override fun userMessage(session: Session, user: User?, userMessage: String, ui: ApplicationInterface, api: API) { - agents[session]?.start(userMessage) - } - override val root: File get() = File(AppSettingsState.instance.pluginHome, "coding_agent") - } - server.addApp(path, codingApp) - return codingApp - } - } -} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AutoDevAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AutoDevAction.kt index 5f949031..937497d1 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AutoDevAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AutoDevAction.kt @@ -4,7 +4,7 @@ import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.actions.dev.AppServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools -import com.github.simiacryptus.aicoder.util.addApplyFileDiffLinks +import com.github.simiacryptus.diff.addApplyFileDiffLinks import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys import com.simiacryptus.jopenai.API @@ -64,6 +64,7 @@ class AutoDevAction : BaseAction() { ) : ApplicationServer( applicationName = applicationName, path = "/autodev", + showMenubar = false, ) { override fun userMessage( session: Session, @@ -227,37 +228,36 @@ class AutoDevAction : BaseAction() { """.trimMargin() } ui.socketManager.addApplyFileDiffLinks( - root = root, - code = codeFiles, - response = taskActor.answer(listOf( - codeSummary(), - userMessage, - filter.entries.joinToString("\n\n") { - "# ${it.key}\n```${ - it.key.split('.').last()?.let { /*escapeHtml4*/it/*.indent(" ")*/ } - }\n${it.value/*.indent(" ")*/}\n```" - }, - architectureResponse.text, - "Provide a change for ${paths?.joinToString(",") { it } ?: ""} ($description)" - ), api), - handle = { newCodeMap -> - newCodeMap.forEach { (path, newCode) -> - val prev = codeFiles[path] - if (prev != newCode) { - codeFiles[path] = newCode - task.complete( - "$path Updated" - ) + root = root, + code = codeFiles, + response = taskActor.answer(listOf( + codeSummary(), + userMessage, + filter.entries.joinToString("\n\n") { + "# ${it.key}\n```${ + it.key.split('.').last()?.let { /*escapeHtml4*/it/*.indent(" ")*/ } + }\n${it.value/*.indent(" ")*/}\n```" + }, + architectureResponse.text, + "Provide a change for ${paths?.joinToString(",") { it } ?: ""} ($description)" + ), api), + handle = { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + val prev = codeFiles[path] + if (prev != newCode) { + codeFiles[path] = newCode + task.complete( + "$path Updated" + ) + } } - } - }, - task = task, - ui = ui + }, + ui = ui ) } Retryable(ui, task, process).apply { set(label(size), process(container!!)) } 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 797f41c6..3d76bc9b 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 @@ -22,54 +22,59 @@ import java.io.File class CodeChatAction : BaseAction() { - val path = "/codeChat" + val path = "/codeChat" - override fun handle(e: AnActionEvent) { - val editor = e.getData(CommonDataKeys.EDITOR) ?: return + override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return - val session = StorageInterface.newGlobalID() - val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return - val filename = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: return - 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) - ) + val session = StorageInterface.newGlobalID() + val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return + val filename = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: return + 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) + ) - val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) + 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")) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() - } + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } - override fun isEnabled(event: AnActionEvent) = true + override fun isEnabled(event: AnActionEvent) = true - 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) { - 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 - } + 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/DiffChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt index 03877e50..cfca05c2 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 @@ -6,7 +6,7 @@ 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.github.simiacryptus.aicoder.util.addApplyDiffLinks +import com.github.simiacryptus.diff.addApplyDiffLinks import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.command.WriteCommandAction @@ -28,44 +28,44 @@ import java.io.File class DiffChatAction : BaseAction() { - val path = "/diffChat" + val path = "/diffChat" - override fun handle(e: AnActionEvent) { - val editor = e.getData(CommonDataKeys.EDITOR) ?: return - val session = StorageInterface.newGlobalID() - val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return - val document = editor.document - val filename = FileDocumentManager.getInstance().getFile(document)?.name ?: return - val primaryCaret = editor.caretModel.primaryCaret - val rawText: String - val selectionStart: Int - val selectionEnd: Int - val selectedText = primaryCaret.selectedText - if (null != selectedText) { - rawText = selectedText - selectionStart = primaryCaret.selectionStart - selectionEnd = primaryCaret.selectionEnd - } else { - rawText = document.text - selectionStart = 0 - selectionEnd = rawText.length - } - val numberedText = rawText.split("\n") - .mapIndexed { lineNumber: Int, lineText: String -> - lineText + override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val session = StorageInterface.newGlobalID() + val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return + val document = editor.document + val filename = FileDocumentManager.getInstance().getFile(document)?.name ?: return + val primaryCaret = editor.caretModel.primaryCaret + val rawText: String + val selectionStart: Int + val selectionEnd: Int + val selectedText = primaryCaret.selectedText + if (null != selectedText) { + rawText = selectedText + selectionStart = primaryCaret.selectionStart + selectionEnd = primaryCaret.selectionEnd + } else { + rawText = document.text + selectionStart = 0 + selectionEnd = rawText.length + } + val numberedText = rawText.split("\n") + .mapIndexed { lineNumber: Int, lineText: String -> + lineText // String.format("%4d: %s", lineNumber + 1, lineText) - }.joinToString("\n") - agents[session] = object : CodeChatSocketManager( - session = session, - language = language, - codeSelection = numberedText, - filename = filename, - api = api, - model = AppSettingsState.instance.smartModel.chatModel(), - storage = ApplicationServices.dataStorageFactory(root) - ) { - override val systemPrompt: String - get() = super.systemPrompt + """ + }.joinToString("\n") + agents[session] = object : CodeChatSocketManager( + session = session, + language = language, + codeSelection = numberedText, + filename = filename, + api = api, + model = AppSettingsState.instance.smartModel.chatModel(), + storage = ApplicationServices.dataStorageFactory(root) + ) { + override val systemPrompt: String + get() = super.systemPrompt + """ Please provide code modifications in the following diff format within triple-backtick diff code blocks. Each diff block should be preceded by a header that identifies the file being modified. The diff format rules are as follows: @@ -86,49 +86,53 @@ class DiffChatAction : BaseAction() { Note: The diff should accurately reflect the changes to be made to the code, including sufficient context to ensure the modifications can be correctly applied. """.trimIndent() - val ui by lazy { ApplicationInterface(this) } - override fun renderResponse(response: String, task: SessionTask): String { - val codeBuffer = StringBuilder(rawText) - val withLinks = addApplyDiffLinks(codeBuffer, response, handle = { newCode: String -> - WriteCommandAction.runWriteCommandAction(e.project) { - document.replaceString(selectionStart, selectionStart + codeBuffer.length, newCode) - } - codeBuffer.set(newCode) - }, task = task, ui=ui) - return """
${renderMarkdown(withLinks)}
""" - } - } + val ui by lazy { ApplicationInterface(this) } + override fun renderResponse(response: String, task: SessionTask): String { + val codeBuffer = StringBuilder(rawText) + val withLinks = addApplyDiffLinks(codeBuffer, response, handle = { newCode: String -> + WriteCommandAction.runWriteCommandAction(e.project) { + document.replaceString(selectionStart, selectionStart + codeBuffer.length, newCode) + } + codeBuffer.set(newCode) + }, task = task, ui = ui) + return """
${renderMarkdown(withLinks)}
""" + } + } - val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) + 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")) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() - } + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } - override fun isEnabled(event: AnActionEvent) = true + override fun isEnabled(event: AnActionEvent) = true - 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("Code Chat", path) { - override val singleInput = false - override val stickyInput = true - override fun newSession(user: User?, session: Session) = agents[session]!! - } - server.addApp(path, socketServer) - return socketServer - } + 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 = "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 + } - } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt index 63b9ffff..f311d568 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt @@ -25,29 +25,29 @@ import java.io.File class LineFilterChatAction : BaseAction() { - val path = "/codeChat" + val path = "/codeChat" - override fun handle(e: AnActionEvent) { - val editor = e.getData(CommonDataKeys.EDITOR) ?: return - val session = StorageInterface.newGlobalID() - val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return - val filename = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: return - val code = editor.caretModel.primaryCaret.selectedText ?: editor.document.text - val lines = code.split("\n").toTypedArray() - val codelines = lines.withIndex().joinToString("\n") { (i, line) -> - "${i.toString().padStart(3, '0')} $line" - } - agents[session] = object : ChatSocketManager( - session = session, - model = AppSettingsState.instance.smartModel.chatModel(), - userInterfacePrompt = """ + override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val session = StorageInterface.newGlobalID() + val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return + val filename = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: return + val code = editor.caretModel.primaryCaret.selectedText ?: editor.document.text + val lines = code.split("\n").toTypedArray() + val codelines = lines.withIndex().joinToString("\n") { (i, line) -> + "${i.toString().padStart(3, '0')} $line" + } + agents[session] = object : ChatSocketManager( + session = session, + model = AppSettingsState.instance.smartModel.chatModel(), + userInterfacePrompt = """ |# `$filename` | |```$language |${code?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} |``` """.trimMargin().trim(), - systemPrompt = """ + systemPrompt = """ |You are a helpful AI that helps people with coding. | |You will be answering questions about the following code located in `$filename`: @@ -71,53 +71,58 @@ class LineFilterChatAction : BaseAction() { |014 |``` """.trimMargin(), - api = api, - applicationClass = ApplicationServer::class.java, - storage = ApplicationServices.dataStorageFactory(root), - ) { - override fun canWrite(user: User?): Boolean = true - override fun renderResponse(response: String, task: SessionTask): String { - return renderMarkdown(response.split("\n").joinToString("\n") { - when { - // Is numeric, use line if in range - it.toIntOrNull()?.let { i -> lines.indices.contains(i) } == true -> lines[it.toInt()] - // Otherwise, use response - else -> it - } + api = api, + applicationClass = ApplicationServer::class.java, + storage = ApplicationServices.dataStorageFactory(root), + ) { + override fun canWrite(user: User?): Boolean = true + override fun renderResponse(response: String, task: SessionTask): String { + return renderMarkdown(response.split("\n").joinToString("\n") { + when { + // Is numeric, use line if in range + it.toIntOrNull()?.let { i -> lines.indices.contains(i) } == true -> lines[it.toInt()] + // Otherwise, use response + else -> it + } + } + ) + } } -) } - } - val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) + 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")) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() - } + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } - override fun isEnabled(event: AnActionEvent) = true + override fun isEnabled(event: AnActionEvent) = true - 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) { - override val singleInput = false - override val stickyInput = true - override fun newSession(user: User?, session: Session) = agents[session]!! - } - server.addApp(path, socketServer) - return socketServer - } + 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/MultiDiffChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt index 00994a28..a8d844a4 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 @@ -6,7 +6,7 @@ import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.AppSettingsState.Companion.chatModel import com.github.simiacryptus.aicoder.util.ComputerLanguage import com.github.simiacryptus.aicoder.util.UITools -import com.github.simiacryptus.aicoder.util.addApplyFileDiffLinks +import com.github.simiacryptus.diff.addApplyFileDiffLinks import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.command.WriteCommandAction @@ -28,54 +28,55 @@ import java.io.File class MultiDiffChatAction : BaseAction() { - val path = "/multiDiffChat" + val path = "/multiDiffChat" - override fun handle(e: AnActionEvent) { + override fun handle(e: AnActionEvent) { - val dataContext = e.dataContext - val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) - val languages = - virtualFiles?.associate { it to ComputerLanguage.findByExtension(it.extension ?: "")?.name } ?: mapOf() - val virtualFileMap = virtualFiles?.associate { it.toNioPath() to it } ?: mapOf() - val codeFiles = mutableMapOf() - val folder = UITools.getSelectedFolder(e) - val root = if (null != folder) { - folder.toFile.toPath() - } else { - getModuleRootForFile(UITools.getSelectedFile(e)?.parent?.toFile ?: throw RuntimeException("")).toPath() - } + val dataContext = e.dataContext + val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) + val languages = + virtualFiles?.associate { it to ComputerLanguage.findByExtension(it.extension ?: "")?.name } ?: mapOf() + val virtualFileMap = virtualFiles?.associate { it.toNioPath() to it } ?: mapOf() + val codeFiles = mutableMapOf() + val folder = UITools.getSelectedFolder(e) + val root = if (null != folder) { + folder.toFile.toPath() + } else { + getModuleRootForFile(UITools.getSelectedFile(e)?.parent?.toFile ?: throw RuntimeException("")).toPath() + } // val root = virtualFiles?.map { file -> // file.toNioPath() // }?.toTypedArray()?.commonRoot()!! - virtualFiles?.associate { file -> - val relative = root.relativize(file.toNioPath()) - val path = relative.toString() - val language = languages[file] ?: "plaintext" - val code = file.contentsToByteArray().toString(Charsets.UTF_8) - codeFiles[path] = code - path to language - } + virtualFiles?.associate { file -> + val relative = root.relativize(file.toNioPath()) + val path = relative.toString() + val language = languages[file] ?: "plaintext" + val code = file.contentsToByteArray().toString(Charsets.UTF_8) + codeFiles[path] = code + path to language + } - fun codeSummary() = codeFiles.entries.joinToString("\n\n") { (path, code) -> - "# $path\n```${ - path.split('.').lastOrNull()?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } - }\n${code?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }}\n```" - } - val session = StorageInterface.newGlobalID() - //DataStorage.sessionPaths[session] = root.toFile() + fun codeSummary() = codeFiles.entries.joinToString("\n\n") { (path, code) -> + "# $path\n```${ + path.split('.').lastOrNull()?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + }\n${code?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }}\n```" + } - val codeSummary = codeSummary() - agents[session] = object : ChatSocketManager( - session = session, - model = AppSettingsState.instance.smartModel.chatModel(), - userInterfacePrompt = """ + val session = StorageInterface.newGlobalID() + //DataStorage.sessionPaths[session] = root.toFile() + + val codeSummary = codeSummary() + agents[session] = object : ChatSocketManager( + session = session, + model = AppSettingsState.instance.smartModel.chatModel(), + userInterfacePrompt = """ | |$codeSummary | """.trimMargin().trim(), - systemPrompt = """ + systemPrompt = """ You are a helpful AI that helps people with coding. You will be answering questions about the following code: @@ -99,70 +100,78 @@ class MultiDiffChatAction : BaseAction() { Continued text """.trimIndent(), - api = api, - applicationClass = ApplicationServer::class.java, - storage = ApplicationServices.dataStorageFactory(DiffChatAction.root), - ) { - override fun renderResponse(response: String, task: SessionTask): String { - val html = addApplyFileDiffLinks( - root = root, - code = codeFiles, - response = response, - handle = { newCodeMap -> - newCodeMap.map { (path, newCode) -> - val prev = codeFiles[path] - if (prev != newCode) { - codeFiles[path] = newCode - root.resolve(path).let { file -> - file.toFile().writeText(newCode) - val virtualFile = virtualFileMap.get(file) - if(null != virtualFile) FileDocumentManager.getInstance().getDocument(virtualFile)?.let { doc -> - WriteCommandAction.runWriteCommandAction(e.project) { - doc.setText(newCode) - } - } - } - "$path Updated" - } else { + api = api, + applicationClass = ApplicationServer::class.java, + storage = ApplicationServices.dataStorageFactory(DiffChatAction.root), + ) { + val ui by lazy { ApplicationInterface(this) } + override fun renderResponse(response: String, task: SessionTask): String { + val html = addApplyFileDiffLinks( + root = root, + code = codeFiles, + response = response, + handle = { newCodeMap -> + newCodeMap.map { (path, newCode) -> + val prev = codeFiles[path] + if (prev != newCode) { + codeFiles[path] = newCode + root.resolve(path).let { file -> + file.toFile().writeText(newCode) + val virtualFile = virtualFileMap.get(file) + if (null != virtualFile) FileDocumentManager.getInstance().getDocument(virtualFile) + ?.let { doc -> + WriteCommandAction.runWriteCommandAction(e.project) { + doc.setText(newCode) + } + } + } + "$path Updated" + } else { // "$path Unchanged" - "" + "" + } + } + }, + ui = ui, + ) + return """
${renderMarkdown(html)}
""" } - } - }, task = task, ui = ApplicationInterface(this),) - return """
${renderMarkdown(html)}
""" - } - } + } - val server = AppServer.getServer(e.project) - val app = initApp(server, path) - app.sessions[session] = app.newSession(null, session) + 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")) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() - } + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } - override fun isEnabled(event: AnActionEvent) = true + override fun isEnabled(event: AnActionEvent) = true - companion object { - private val log = LoggerFactory.getLogger(MultiDiffChatAction::class.java) - private val agents = mutableMapOf() - val root: File get() = File(AppSettingsState.instance.pluginHome, "mdiff_chat") - private fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer("Multi-file Diff Chat", path) { - override val singleInput = false - override val stickyInput = true - override fun newSession(user: User?, session: Session) = agents[session]!! - } - server.addApp(path, socketServer) - return socketServer - } + companion object { + private val log = LoggerFactory.getLogger(MultiDiffChatAction::class.java) + private val agents = mutableMapOf() + val root: File get() = File(AppSettingsState.instance.pluginHome, "mdiff_chat") + private fun initApp(server: AppServer, path: String): ChatServer { + server.appRegistry[path]?.let { return it } + val socketServer = object : ApplicationServer( + applicationName = "Multi-file Diff 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/TaskRunnerAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/TaskRunnerAction.kt index 4e5ffabe..280ba458 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/TaskRunnerAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/TaskRunnerAction.kt @@ -5,8 +5,8 @@ import com.github.simiacryptus.aicoder.actions.dev.AppServer 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.github.simiacryptus.aicoder.util.addApplyFileDiffLinks -import com.github.simiacryptus.aicoder.util.addSaveLinks +import com.github.simiacryptus.diff.addApplyFileDiffLinks +import com.github.simiacryptus.diff.addSaveLinks import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys.VIRTUAL_FILE_ARRAY import com.intellij.openapi.vfs.VirtualFile @@ -49,120 +49,125 @@ import kotlin.reflect.KClass class TaskRunnerAction : BaseAction() { - val path = "/taskDev" - override fun handle(e: AnActionEvent) { - val session = StorageInterface.newGlobalID() - val folder = UITools.getSelectedFolder(e) - val root = if (null != folder) { - folder.toFile - } else { - getModuleRootForFile(UITools.getSelectedFile(e)?.parent?.toFile ?: throw RuntimeException("")) + val path = "/taskDev" + override fun handle(e: AnActionEvent) { + val session = StorageInterface.newGlobalID() + val folder = UITools.getSelectedFolder(e) + val root = if (null != folder) { + folder.toFile + } else { + getModuleRootForFile(UITools.getSelectedFile(e)?.parent?.toFile ?: throw RuntimeException("")) + } + DataStorage.sessionPaths[session] = root + TaskRunnerApp.agents[session] = TaskRunnerApp(event = e, root = root) + val server = AppServer.getServer(e.project) + val app = TaskRunnerApp.initApp(server, path) + app.sessions[session] = app.newSession(null, session) + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() } - DataStorage.sessionPaths[session] = root - TaskRunnerApp.agents[session] = TaskRunnerApp(event = e, root = root) - val server = AppServer.getServer(e.project) - val app = TaskRunnerApp.initApp(server, path) - app.sessions[session] = app.newSession(null, session) - Thread { - Thread.sleep(500) - try { - Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() - } } class TaskRunnerApp( - applicationName: String = "Task Planning v1.0", - path: String = "/taskDev", - val event: AnActionEvent, - override val root: File, + applicationName: String = "Task Planning v1.0", + path: String = "/taskDev", + val event: AnActionEvent, + override val root: File, ) : ApplicationServer( - applicationName = applicationName, - path = path, + applicationName = applicationName, + path = path, + showMenubar = false, ) { - data class Settings( - val model: ChatModels = AppSettingsState.instance.smartModel.chatModel(), - val parsingModel: ChatModels = AppSettingsState.instance.fastModel.chatModel(), - val temperature: Double = 0.2, - val budget: Double = 2.0, - val taskPlanningEnabled: Boolean = false, - val shellCommandTaskEnabled: Boolean = true, - ) + data class Settings( + val model: ChatModels = AppSettingsState.instance.smartModel.chatModel(), + val parsingModel: ChatModels = AppSettingsState.instance.fastModel.chatModel(), + val temperature: Double = 0.2, + val budget: Double = 2.0, + val taskPlanningEnabled: Boolean = false, + val shellCommandTaskEnabled: Boolean = true, + ) - override val settingsClass: Class<*> get() = Settings::class.java + override val settingsClass: Class<*> get() = Settings::class.java - @Suppress("UNCHECKED_CAST") - override fun initSettings(session: Session): T? = Settings() as T + @Suppress("UNCHECKED_CAST") + override fun initSettings(session: Session): T? = Settings() as T - override fun userMessage( - session: Session, - user: User?, - userMessage: String, - ui: ApplicationInterface, - api: API - ) { - try { - val settings = getSettings(session, user) - if (api is ClientManager.MonitoredClient) api.budget = settings?.budget ?: 2.0 - TaskRunnerAgent( - user = user, - session = session, - dataStorage = dataStorage, - api = api, - ui = ui, - model = settings?.model ?: AppSettingsState.instance.smartModel.chatModel(), - parsingModel = settings?.parsingModel ?: AppSettingsState.instance.fastModel.chatModel(), - temperature = settings?.temperature ?: 0.3, - event = event, - root = root.toPath(), - taskPlanningEnabled = false, - shellCommandTaskEnabled = false, - ).startProcess(userMessage = userMessage) - } catch (e: Throwable) { - ui.newTask().error(ui, e) - log.warn("Error", e) + override fun userMessage( + session: Session, + user: User?, + userMessage: String, + ui: ApplicationInterface, + api: API + ) { + try { + val settings = getSettings(session, user) + if (api is ClientManager.MonitoredClient) api.budget = settings?.budget ?: 2.0 + TaskRunnerAgent( + user = user, + session = session, + dataStorage = dataStorage, + api = api, + ui = ui, + model = settings?.model ?: AppSettingsState.instance.smartModel.chatModel(), + parsingModel = settings?.parsingModel ?: AppSettingsState.instance.fastModel.chatModel(), + temperature = settings?.temperature ?: 0.3, + event = event, + root = root.toPath(), + taskPlanningEnabled = false, + shellCommandTaskEnabled = false, + ).startProcess(userMessage = userMessage) + } catch (e: Throwable) { + ui.newTask().error(ui, e) + log.warn("Error", e) + } } - } - companion object { - fun initApp(server: AppServer, path: String): ChatServer { - server.appRegistry[path]?.let { return it } - val socketServer = object : ApplicationServer(applicationName = "Task Agent", 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 - } + companion object { + fun initApp(server: AppServer, path: String): ChatServer { + server.appRegistry[path]?.let { return it } + val socketServer = object : ApplicationServer( + applicationName = "Task 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 + } - private val log = LoggerFactory.getLogger(TaskRunnerApp::class.java) - val agents = mutableMapOf() - } + private val log = LoggerFactory.getLogger(TaskRunnerApp::class.java) + val agents = mutableMapOf() + } } class TaskRunnerAgent( - user: User?, - session: Session, - dataStorage: StorageInterface, - val ui: ApplicationInterface, - val api: API, - model: ChatModels = ChatModels.GPT4Turbo, - parsingModel: ChatModels = ChatModels.GPT35Turbo, - temperature: Double = 0.3, - val taskPlanningEnabled: Boolean, - val shellCommandTaskEnabled: Boolean, - val env: Map = mapOf(), - val workingDir: String = ".", - val language: String = if (isWindows) "powershell" else "bash", - val command: List = listOf(language), - val actorMap: Map> = mapOf( - ActorTypes.TaskBreakdown to ParsedActor( - resultClass = TaskBreakdownResult::class.java, - prompt = """ + user: User?, + session: Session, + dataStorage: StorageInterface, + val ui: ApplicationInterface, + val api: API, + model: ChatModels = ChatModels.GPT4Turbo, + parsingModel: ChatModels = ChatModels.GPT35Turbo, + temperature: Double = 0.3, + val taskPlanningEnabled: Boolean, + val shellCommandTaskEnabled: Boolean, + val env: Map = mapOf(), + val workingDir: String = ".", + val language: String = if (isWindows) "powershell" else "bash", + val command: List = listOf(language), + val actorMap: Map> = mapOf( + ActorTypes.TaskBreakdown to ParsedActor( + resultClass = TaskBreakdownResult::class.java, + prompt = """ |Given a user request, identify and list smaller, actionable tasks that can be directly implemented in code. |Detail files input and output as well as task execution dependencies. |Keep in mind that implementation details need to be shared between the file generation tasks. @@ -170,19 +175,18 @@ class TaskRunnerAgent( | |Tasks can be of the following types: |${ - if (!taskPlanningEnabled) "" else - """ + if (!taskPlanningEnabled) "" else """ |* TaskPlanning - High-level planning and organization of tasks - identify smaller, actionable tasks based on the information available at task execution time. | ** Specify the prior tasks and the goal of the task """.trimMargin().trim() - } + } |${ - if (!shellCommandTaskEnabled) "" else """ + if (!shellCommandTaskEnabled) "" else """ |* RunShellCommand - Execute shell commands and provide the output | ** Specify the command to be executed, or describe the task to be performed | ** List input files/tasks to be examined when writing the command """.trimMargin().trim() - } + } |* Inquiry - Answer questions by reading in files and providing a summary that can be discussed with and approved by the user | ** Specify the questions and the goal of the inquiry | ** List input files to be examined when answering the questions @@ -195,24 +199,24 @@ class TaskRunnerAgent( |* Documentation - Generate documentation | ** List input files/tasks to be examined """.trimMargin(), - model = model, - parsingModel = parsingModel, - temperature = temperature, - ), - ActorTypes.DocumentationGenerator to SimpleActor( - name = "DocumentationGenerator", - prompt = """ + model = model, + parsingModel = parsingModel, + temperature = temperature, + ), + ActorTypes.DocumentationGenerator to SimpleActor( + name = "DocumentationGenerator", + prompt = """ Create detailed and clear documentation for the provided code, covering its purpose, functionality, inputs, outputs, and any assumptions or limitations. Use a structured and consistent format that facilitates easy understanding and navigation. Include code examples where applicable, and explain the rationale behind key design decisions and algorithm choices. Document any known issues or areas for improvement, providing guidance for future developers on how to extend or maintain the code. """.trimIndent(), - model = model, - temperature = temperature, - ), - ActorTypes.NewFileCreator to SimpleActor( - name = "NewFileCreator", - prompt = """ + model = model, + temperature = temperature, + ), + ActorTypes.NewFileCreator to SimpleActor( + name = "NewFileCreator", + prompt = """ Generate the necessary code for a new file based on the given requirements and context. Ensure the code is well-structured, follows best practices, and meets the specified functionality. Carefully consider how the new file fits into the existing project structure and architecture. @@ -239,12 +243,12 @@ class TaskRunnerAgent( Continued text """.trimIndent(), - model = model, - temperature = temperature, - ), - ActorTypes.FilePatcher to SimpleActor( - name = "FilePatcher", - prompt = """ + model = model, + temperature = temperature, + ), + ActorTypes.FilePatcher to SimpleActor( + name = "FilePatcher", + prompt = """ Generate a patch for an existing file to modify its functionality or fix issues based on the given requirements and context. Ensure the modifications are efficient, maintain readability, and adhere to coding standards. Carefully review the existing code and project structure to ensure the changes are consistent and do not introduce bugs. @@ -269,12 +273,12 @@ class TaskRunnerAgent( Continued text """.trimIndent(), - model = model, - temperature = temperature, - ), - ActorTypes.Inquiry to SimpleActor( - name = "Inquiry", - prompt = """ + model = model, + temperature = temperature, + ), + ActorTypes.Inquiry to SimpleActor( + name = "Inquiry", + prompt = """ Create code for a new file that fulfills the specified requirements and context. Given a detailed user request, break it down into smaller, actionable tasks suitable for software development. Compile comprehensive information and insights on the specified topic. @@ -283,300 +287,303 @@ class TaskRunnerAgent( When generating insights, consider the existing project context and focus on information that is directly relevant and applicable. Focus on generating insights and information that support the task types available in the system (${ - if (!taskPlanningEnabled) "" else "TaskPlanning, " - }${ - if (!shellCommandTaskEnabled) "" else "RunShellCommand, " - }Requirements, NewFile, EditFile, Documentation). + if (!taskPlanningEnabled) "" else "TaskPlanning, " + }${ + if (!shellCommandTaskEnabled) "" else "RunShellCommand, " + }Requirements, NewFile, EditFile, Documentation). This will ensure that the inquiries are tailored to assist in the planning and execution of tasks within the system's framework. """.trimIndent(), - model = model, - temperature = temperature, - ), - ) + (if (!shellCommandTaskEnabled) mapOf() else mapOf( - ActorTypes.RunShellCommand to CodingActor( - name = "RunShellCommand", - interpreterClass = ProcessInterpreter::class, - details = """ + model = model, + temperature = temperature, + ), + ) + (if (!shellCommandTaskEnabled) mapOf() else mapOf( + ActorTypes.RunShellCommand to CodingActor( + name = "RunShellCommand", + interpreterClass = ProcessInterpreter::class, + details = """ Execute the following shell command(s) and provide the output. Ensure to handle any errors or exceptions gracefully. Note: This task is for running simple and safe commands. Avoid executing commands that can cause harm to the system or compromise security. """.trimIndent(), - symbols = mapOf( - "env" to (env ?: mapOf()), - "workingDir" to File(workingDir ?: ".").absolutePath, - "language" to (language ?: "bash"), - "command" to (command ?: listOf("bash")), - ), - model = model, - temperature = temperature, - ), - )), + symbols = mapOf( + "env" to (env ?: mapOf()), + "workingDir" to File(workingDir ?: ".").absolutePath, + "language" to (language ?: "bash"), + "command" to (command ?: listOf("bash")), + ), + model = model, + temperature = temperature, + ), + )), - val event: AnActionEvent, - val root: Path + val event: AnActionEvent, + val root: Path ) : ActorSystem( - actorMap.map { it.key.name to it.value }.toMap(), - dataStorage, - user, - session + actorMap.map { it.key.name to it.value }.toMap(), + dataStorage, + user, + session ) { - val documentationGeneratorActor by lazy { actorMap[ActorTypes.DocumentationGenerator] as SimpleActor } - val taskBreakdownActor by lazy { actorMap[ActorTypes.TaskBreakdown] as ParsedActor } - val newFileCreatorActor by lazy { actorMap[ActorTypes.NewFileCreator] as SimpleActor } - val filePatcherActor by lazy { actorMap[ActorTypes.FilePatcher] as SimpleActor } - val inquiryActor by lazy { actorMap[ActorTypes.Inquiry] as SimpleActor } - val shellCommandActor by lazy { actorMap[ActorTypes.RunShellCommand] as CodingActor } - - data class TaskBreakdownResult( - val tasksByID: Map? = null, - val finalTaskID: String? = null, - ) + val documentationGeneratorActor by lazy { actorMap[ActorTypes.DocumentationGenerator] as SimpleActor } + val taskBreakdownActor by lazy { actorMap[ActorTypes.TaskBreakdown] as ParsedActor } + val newFileCreatorActor by lazy { actorMap[ActorTypes.NewFileCreator] as SimpleActor } + val filePatcherActor by lazy { actorMap[ActorTypes.FilePatcher] as SimpleActor } + val inquiryActor by lazy { actorMap[ActorTypes.Inquiry] as SimpleActor } + val shellCommandActor by lazy { actorMap[ActorTypes.RunShellCommand] as CodingActor } - data class Task( - val description: String? = null, - val taskType: TaskType? = null, - var task_dependencies: List? = null, - val input_files: List? = null, - val output_files: List? = null, - var state: TaskState? = null, - ) + data class TaskBreakdownResult( + val tasksByID: Map? = null, + val finalTaskID: String? = null, + ) - enum class TaskState { - Pending, - InProgress, - Completed, - } + data class Task( + val description: String? = null, + val taskType: TaskType? = null, + var task_dependencies: List? = null, + val input_files: List? = null, + val output_files: List? = null, + var state: TaskState? = null, + ) - enum class TaskType { - TaskPlanning, - Inquiry, - NewFile, - EditFile, - Documentation, - RunShellCommand, - } + enum class TaskState { + Pending, + InProgress, + Completed, + } -// val root by lazy { -// VIRTUAL_FILE_ARRAY.getData(event.dataContext) -// ?.map { it.toFile.toPath() }?.toTypedArray() -// ?.commonRoot()!! -// } + enum class TaskType { + TaskPlanning, + Inquiry, + NewFile, + EditFile, + Documentation, + RunShellCommand, + } - val virtualFiles by lazy { - expandFileList( - VIRTUAL_FILE_ARRAY.getData(event.dataContext) ?: arrayOf() - ) - } + val virtualFiles by lazy { + expandFileList( + VIRTUAL_FILE_ARRAY.getData(event.dataContext) ?: arrayOf() + ) + } - private fun expandFileList(data: Array): Array { - return data.flatMap { - (when { - it.name.startsWith(".") -> arrayOf() - it.length > 1e6 -> arrayOf() - it.extension?.lowercase(Locale.getDefault()) in - setOf("jar", "zip", "class", "png", "jpg", "jpeg", "gif", "ico") -> arrayOf() + private fun expandFileList(data: Array): Array { + return data.flatMap { + (when { + it.name.startsWith(".") -> arrayOf() + it.length > 1e6 -> arrayOf() + it.extension?.lowercase(Locale.getDefault()) in + setOf("jar", "zip", "class", "png", "jpg", "jpeg", "gif", "ico") -> arrayOf() - it.isDirectory -> expandFileList(it.children) - else -> arrayOf(it) - }).toList() - }.toTypedArray() - } + it.isDirectory -> expandFileList(it.children) + else -> arrayOf(it) + }).toList() + }.toTypedArray() + } - val codeFiles = mutableMapOf().apply { - virtualFiles.filter { it.isFile }.forEach { file -> - val code = file.inputStream.bufferedReader().use { it.readText() } - this[root.relativize(file.toNioPath()).toString()] = code + val codeFiles = mutableMapOf().apply { + virtualFiles.filter { it.isFile }.forEach { file -> + val code = file.inputStream.bufferedReader().use { it.readText() } + this[root.relativize(file.toNioPath()).toString()] = code + } } - } - fun startProcess(userMessage: String) { - val codeFiles = codeFiles - val eventStatus = if (!codeFiles.all { File(it.key).isFile } || codeFiles.size > 2) """ + fun startProcess(userMessage: String) { + val codeFiles = codeFiles + val eventStatus = if (!codeFiles.all { File(it.key).isFile } || codeFiles.size > 2) """ |Files: |${codeFiles.keys.joinToString("\n") { "* ${it}" }} """.trimMargin() else { - """ + """ |${ - virtualFiles.joinToString("\n\n") { - val path = root.relativize(it.toNioPath()) - """ + virtualFiles.joinToString("\n\n") { + val path = root.relativize(it.toNioPath()) + """ |## $path | |${(codeFiles[path.toString()] ?: "").let { "```\n${it/*.indent(" ")*/}\n```" }} """.trimMargin() - } - } - """.trimMargin() - } - val task = ui.newTask() - val toInput = { it: String -> - listOf( - eventStatus, - it - ) - } - val highLevelPlan = Acceptable( - task = task, - heading = renderMarkdown(userMessage, ui=ui), - userMessage = userMessage, - initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, - outputFn = { design: ParsedResponse -> - displayMapInTabs( - mapOf( - "Text" to renderMarkdown(design.text, ui=ui), - "JSON" to renderMarkdown("```json\n${toJson(design.obj)/*.indent(" ")*/}\n```", ui=ui), - ) - ) - }, - ui = ui, - reviseResponse = { userMessages: List> -> - taskBreakdownActor.respond( - messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } - .toTypedArray()), - input = toInput(userMessage), - api = api - ) - }, - ).call() - - try { - val tasksByID = highLevelPlan.obj.tasksByID?.entries?.toTypedArray()?.associate { it.key to it.value } ?: mapOf() - val pool: ThreadPoolExecutor = clientManager.getPool(session, user, dataStorage) - val genState = GenState(tasksByID.toMutableMap()) - val diagramTask = ui.newTask() - val diagramBuffer = - diagramTask.add(renderMarkdown("## Task Dependency Graph\n```mermaid\n${buildMermaidGraph(genState.subTasks)}\n```", ui=ui)) - val taskTabs = object : TabbedDisplay(ui.newTask()) { - override fun renderTabButtons(): String { - diagramBuffer?.set(renderMarkdown("## Task Dependency Graph\n```mermaid\n${buildMermaidGraph(genState.subTasks)}\n```", ui=ui)) - diagramTask.complete() - return buildString { - append("
\n") - super.tabs.withIndex().forEach { (idx, t) -> - val (taskId, taskV) = t - val subTask = genState.tasksByDescription[taskId] - if (null == subTask) { - log.warn("Task tab not found: $taskId") - } - val isChecked = if (taskId in genState.taskIdProcessingQueue) "checked" else "" -// log.debug("Task: '${subTask?.state}' ${System.identityHashCode(subTask)} '${taskId}' ") - val style = when (subTask?.state) { - TaskState.Completed -> " style='text-decoration: line-through;'" - null -> " style='opacity: 20%;'" - TaskState.Pending -> " style='opacity: 30%;'" - else -> "" - } - append("
\n") + } } - append("
") - } + """.trimMargin() } - } - genState.taskIdProcessingQueue.forEach { taskId -> - val newTask = ui.newTask() - genState.uitaskMap[taskId] = newTask - val subtask = genState.subTasks[taskId] - val description = subtask?.description - log.debug("Creating task tab: $taskId ${System.identityHashCode(subtask)} $description") - taskTabs[description ?: taskId] = "
" - } - Thread.sleep(100) - while (genState.taskIdProcessingQueue.isNotEmpty()) { - val taskId = genState.taskIdProcessingQueue.removeAt(0) - val subTask = genState.subTasks[taskId] ?: throw RuntimeException("Task not found: $taskId") - genState.taskFutures[taskId] = pool.submit { - subTask.state = TaskState.Pending - taskTabs.update() - log.debug("Awaiting dependencies: ${subTask.task_dependencies?.joinToString(", ") ?: ""}") - subTask.task_dependencies - ?.associate { it to genState.taskFutures[it] } - ?.forEach { (id, future) -> - try { - future?.get() ?: log.warn("Dependency not found: $id") - } catch (e: Throwable) { - log.warn("Error", e) - } - } - subTask.state = TaskState.InProgress - taskTabs.update() - log.debug("Running task: ${System.identityHashCode(subTask)} ${subTask.description}") - runTask( - taskId = taskId, - subTask = subTask, - userMessage = userMessage, - highLevelPlan = highLevelPlan, - genState = genState, - task = genState.uitaskMap.get(taskId) ?: ui.newTask(), - taskTabs = taskTabs - ) + val task = ui.newTask() + val toInput = { it: String -> + listOf( + eventStatus, + it + ) } - } - genState.taskFutures.forEach { (id, future) -> + val highLevelPlan = Acceptable( + task = task, + heading = renderMarkdown(userMessage, ui = ui), + userMessage = userMessage, + initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, + outputFn = { design: ParsedResponse -> + displayMapInTabs( + mapOf( + "Text" to renderMarkdown(design.text, ui = ui), + "JSON" to renderMarkdown("```json\n${toJson(design.obj)/*.indent(" ")*/}\n```", ui = ui), + ) + ) + }, + ui = ui, + reviseResponse = { userMessages: List> -> + taskBreakdownActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput(userMessage), + api = api + ) + }, + ).call() + try { - future.get() ?: log.warn("Dependency not found: $id") + val tasksByID = + highLevelPlan.obj.tasksByID?.entries?.toTypedArray()?.associate { it.key to it.value } ?: mapOf() + val pool: ThreadPoolExecutor = clientManager.getPool(session, user, dataStorage) + val genState = GenState(tasksByID.toMutableMap()) + val diagramTask = ui.newTask() + val diagramBuffer = + diagramTask.add( + renderMarkdown( + "## Task Dependency Graph\n```mermaid\n${buildMermaidGraph(genState.subTasks)}\n```", + ui = ui + ) + ) + val taskTabs = object : TabbedDisplay(ui.newTask()) { + override fun renderTabButtons(): String { + diagramBuffer?.set( + renderMarkdown( + "## Task Dependency Graph\n```mermaid\n${ + buildMermaidGraph( + genState.subTasks + ) + }\n```", ui = ui + ) + ) + diagramTask.complete() + return buildString { + append("
\n") + super.tabs.withIndex().forEach { (idx, t) -> + val (taskId, taskV) = t + val subTask = genState.tasksByDescription[taskId] + if (null == subTask) { + log.warn("Task tab not found: $taskId") + } + val isChecked = if (taskId in genState.taskIdProcessingQueue) "checked" else "" + val style = when (subTask?.state) { + TaskState.Completed -> " style='text-decoration: line-through;'" + null -> " style='opacity: 20%;'" + TaskState.Pending -> " style='opacity: 30%;'" + else -> "" + } + append("
\n") + } + append("
") + } + } + } + genState.taskIdProcessingQueue.forEach { taskId -> + val newTask = ui.newTask() + genState.uitaskMap[taskId] = newTask + val subtask = genState.subTasks[taskId] + val description = subtask?.description + log.debug("Creating task tab: $taskId ${System.identityHashCode(subtask)} $description") + taskTabs[description ?: taskId] = "
" + } + Thread.sleep(100) + while (genState.taskIdProcessingQueue.isNotEmpty()) { + val taskId = genState.taskIdProcessingQueue.removeAt(0) + val subTask = genState.subTasks[taskId] ?: throw RuntimeException("Task not found: $taskId") + genState.taskFutures[taskId] = pool.submit { + subTask.state = TaskState.Pending + taskTabs.update() + log.debug("Awaiting dependencies: ${subTask.task_dependencies?.joinToString(", ") ?: ""}") + subTask.task_dependencies + ?.associate { it to genState.taskFutures[it] } + ?.forEach { (id, future) -> + try { + future?.get() ?: log.warn("Dependency not found: $id") + } catch (e: Throwable) { + log.warn("Error", e) + } + } + subTask.state = TaskState.InProgress + taskTabs.update() + log.debug("Running task: ${System.identityHashCode(subTask)} ${subTask.description}") + runTask( + taskId = taskId, + subTask = subTask, + userMessage = userMessage, + highLevelPlan = highLevelPlan, + genState = genState, + task = genState.uitaskMap.get(taskId) ?: ui.newTask(), + taskTabs = taskTabs + ) + } + } + genState.taskFutures.forEach { (id, future) -> + try { + future.get() ?: log.warn("Dependency not found: $id") + } catch (e: Throwable) { + log.warn("Error", e) + } + } } catch (e: Throwable) { - log.warn("Error", e) + log.warn("Error during incremental code generation process", e) + task.error(ui, e) } - } - } catch (e: Throwable) { - log.warn("Error during incremental code generation process", e) - task.error(ui, e) } - } - - private fun expandPaths(files: Array?) = files?.toList()?.flatMap { - if (it.isDirectory) it.children.toList() else listOf(it) - }?.map { it.toFile.toPath() }?.toTypedArray() ?: arrayOf() - data class GenState( - val subTasks: MutableMap, - val tasksByDescription: MutableMap = subTasks.entries.toTypedArray() - .associate { it.value.description to it.value }.toMutableMap(), - val taskIdProcessingQueue: MutableList = executionOrder(subTasks).toMutableList(), - val taskResult: MutableMap = mutableMapOf(), - val completedTasks: MutableList = mutableListOf(), - val taskFutures: MutableMap> = mutableMapOf(), - val uitaskMap: MutableMap = mutableMapOf(), - ) + data class GenState( + val subTasks: MutableMap, + val tasksByDescription: MutableMap = subTasks.entries.toTypedArray() + .associate { it.value.description to it.value }.toMutableMap(), + val taskIdProcessingQueue: MutableList = executionOrder(subTasks).toMutableList(), + val taskResult: MutableMap = mutableMapOf(), + val completedTasks: MutableList = mutableListOf(), + val taskFutures: MutableMap> = mutableMapOf(), + val uitaskMap: MutableMap = mutableMapOf(), + ) - private fun runTask( - taskId: String, - subTask: Task, - userMessage: String, - highLevelPlan: ParsedResponse, - genState: GenState, - task: SessionTask, - taskTabs: TabbedDisplay, - ) { - try { - val dependencies = subTask.task_dependencies?.toMutableSet() ?: mutableSetOf() - dependencies += getAllDependencies(subTask, genState.subTasks) - val priorCode = dependencies - .joinToString("\n\n\n") { dependency -> - """ + private fun runTask( + taskId: String, + subTask: Task, + userMessage: String, + highLevelPlan: ParsedResponse, + genState: GenState, + task: SessionTask, + taskTabs: TabbedDisplay, + ) { + try { + val dependencies = subTask.task_dependencies?.toMutableSet() ?: mutableSetOf() + dependencies += getAllDependencies(subTask, genState.subTasks) + val priorCode = dependencies + .joinToString("\n\n\n") { dependency -> + """ |# $dependency | |${genState.taskResult[dependency] ?: ""} """.trimMargin() - } - val codeFiles = codeFiles - val inputFileCode = subTask.input_files?.joinToString("\n\n\n") { - try { - """ + } + val codeFiles = codeFiles + fun inputFileCode() = subTask.input_files?.joinToString("\n\n\n") { + try { + """ |# $it | |``` |${codeFiles[it] ?: root.resolve(it).toFile().readText()} |``` """.trimMargin() - } catch (e: Throwable) { - log.warn("Error: root=$root ", e) - "" - } - } ?: "" - task.add( - renderMarkdown( - """ + } catch (e: Throwable) { + log.warn("Error: root=$root ", e) + "" + } + } ?: "" + task.add( + renderMarkdown( + """ |## Task `${taskId}` |${subTask.description ?: ""} | @@ -587,584 +594,582 @@ class TaskRunnerAgent( |### Dependencies: |${dependencies.joinToString("\n") { "- $it" }} | - """.trimMargin(), ui=ui - ) - ) + """.trimMargin(), ui = ui + ) + ) - when (subTask.taskType) { + when (subTask.taskType) { - TaskType.NewFile -> { - val semaphore = Semaphore(0) - createFiles( - task = task, - userMessage = userMessage, - highLevelPlan = highLevelPlan, - priorCode = priorCode, - inputFileCode = inputFileCode, - subTask = subTask, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { semaphore.release() } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) - } + TaskType.NewFile -> { + val semaphore = Semaphore(0) + createFiles( + task = task, + userMessage = userMessage, + highLevelPlan = highLevelPlan, + priorCode = priorCode, + inputFileCode = ::inputFileCode, + subTask = subTask, + genState = genState, + taskId = taskId, + taskTabs = taskTabs, + ) { semaphore.release() } + try { + semaphore.acquire() + } catch (e: Throwable) { + log.warn("Error", e) + } - } + } - TaskType.EditFile -> { - val semaphore = Semaphore(0) - editFiles( - task = task, - userMessage = userMessage, - highLevelPlan = highLevelPlan, - priorCode = priorCode, - inputFileCode = inputFileCode, - subTask = subTask, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { semaphore.release() } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) - } - } + TaskType.EditFile -> { + val semaphore = Semaphore(0) + editFiles( + task = task, + userMessage = userMessage, + highLevelPlan = highLevelPlan, + priorCode = priorCode, + inputFileCode = ::inputFileCode, + subTask = subTask, + genState = genState, + taskId = taskId, + taskTabs = taskTabs, + ) { semaphore.release() } + try { + semaphore.acquire() + } catch (e: Throwable) { + log.warn("Error", e) + } + } - TaskType.Documentation -> { - val semaphore = Semaphore(0) - document( - task = task, - userMessage = userMessage, - highLevelPlan = highLevelPlan, - priorCode = priorCode, - inputFileCode = inputFileCode, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { - semaphore.release() - } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) - } - } + TaskType.Documentation -> { + val semaphore = Semaphore(0) + document( + task = task, + userMessage = userMessage, + highLevelPlan = highLevelPlan, + priorCode = priorCode, + inputFileCode = ::inputFileCode, + genState = genState, + taskId = taskId, + taskTabs = taskTabs, + ) { + semaphore.release() + } + try { + semaphore.acquire() + } catch (e: Throwable) { + log.warn("Error", e) + } + } - TaskType.Inquiry -> { - inquiry( - subTask = subTask, - userMessage = userMessage, - highLevelPlan = highLevelPlan, - priorCode = priorCode, - inputFileCode = inputFileCode, - genState = genState, - taskId = taskId, - task = task, - taskTabs = taskTabs, - ) - } + TaskType.Inquiry -> { + inquiry( + subTask = subTask, + userMessage = userMessage, + highLevelPlan = highLevelPlan, + priorCode = priorCode, + inputFileCode = ::inputFileCode, + genState = genState, + taskId = taskId, + task = task, + taskTabs = taskTabs, + ) + } - TaskType.TaskPlanning -> { - if (taskPlanningEnabled) taskPlanning( - subTask = subTask, - userMessage = userMessage, - highLevelPlan = highLevelPlan, - priorCode = priorCode, - inputFileCode = inputFileCode, - genState = genState, - taskId = taskId, - task = task, - taskTabs = taskTabs, - ) - } + TaskType.TaskPlanning -> { + if (taskPlanningEnabled) taskPlanning( + subTask = subTask, + userMessage = userMessage, + highLevelPlan = highLevelPlan, + priorCode = priorCode, + inputFileCode = ::inputFileCode, + genState = genState, + taskId = taskId, + task = task, + taskTabs = taskTabs, + ) + } - TaskType.RunShellCommand -> { - if (shellCommandTaskEnabled) { - val semaphore = Semaphore(0) - runShellCommand( - task = task, - userMessage = userMessage, - highLevelPlan = highLevelPlan, - priorCode = priorCode, - inputFileCode = inputFileCode, - genState = genState, - taskId = taskId, - taskTabs = taskTabs, - ) { - semaphore.release() - } - try { - semaphore.acquire() - } catch (e: Throwable) { - log.warn("Error", e) + TaskType.RunShellCommand -> { + if (shellCommandTaskEnabled) { + val semaphore = Semaphore(0) + runShellCommand( + task = task, + userMessage = userMessage, + highLevelPlan = highLevelPlan, + priorCode = priorCode, + inputFileCode = ::inputFileCode, + genState = genState, + taskId = taskId, + taskTabs = taskTabs, + ) { + semaphore.release() + } + try { + semaphore.acquire() + } catch (e: Throwable) { + log.warn("Error", e) + } + log.debug("Completed shell command: $taskId") + } + } + + else -> null } - log.debug("Completed shell command: $taskId") - } + } catch (e: Exception) { + log.warn("Error during task execution", e) + task.error(ui, e) + } finally { + genState.completedTasks.add(taskId) + subTask.state = TaskState.Completed + log.debug("Completed task: $taskId ${System.identityHashCode(subTask)}") + taskTabs.update() } - - else -> null - } - } catch (e: Exception) { - log.warn("Error during task execution", e) - task.error(ui, e) - } finally { - genState.completedTasks.add(taskId) - subTask.state = TaskState.Completed - log.debug("Completed task: $taskId ${System.identityHashCode(subTask)}") - taskTabs.update() } - } - private fun runShellCommand( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: String, - genState: TaskRunnerAgent.GenState, - taskId: String, - taskTabs: TabbedDisplay, - function: () -> Unit - ) { - object : CodingAgent( - api = api, - dataStorage = dataStorage, - session = session, - user = user, - ui = ui, - interpreter = shellCommandActor.interpreterClass as KClass, - symbols = shellCommandActor.symbols, - temperature = shellCommandActor.temperature, - details = shellCommandActor.details, - model = shellCommandActor.model, - ) { - override fun displayFeedback( + private fun runShellCommand( task: SessionTask, - request: CodingActor.CodeRequest, - response: CodingActor.CodeResult - ) { - val formText = StringBuilder() - var formHandle: StringBuilder? = null - formHandle = task.add( - """ - |
- |${if (!super.canPlay) "" else super.playButton(task, request, response, formText) { formHandle!! }} - |${acceptButton(task, request, response, formText) { formHandle!! }} - |
- |${super.reviseMsg(task, request, response, formText) { formHandle!! }} - """.trimMargin(), className = "reply-message" - ) - formText.append(formHandle.toString()) - formHandle.toString() - task.complete() - } + userMessage: String, + highLevelPlan: ParsedResponse, + priorCode: String, + inputFileCode: () -> String, + genState: GenState, + taskId: String, + taskTabs: TabbedDisplay, + function: () -> Unit + ) { + object : CodingAgent( + api = api, + dataStorage = dataStorage, + session = session, + user = user, + ui = ui, + interpreter = shellCommandActor.interpreterClass as KClass, + symbols = shellCommandActor.symbols, + temperature = shellCommandActor.temperature, + details = shellCommandActor.details, + model = shellCommandActor.model, + ) { + override fun displayFeedback( + task: SessionTask, + request: CodingActor.CodeRequest, + response: CodingActor.CodeResult + ) { + val formText = StringBuilder() + var formHandle: StringBuilder? = null + formHandle = task.add( + """ + |
+ |${if (!super.canPlay) "" else super.playButton(task, request, response, formText) { formHandle!! }} + |${acceptButton(task, request, response, formText) { formHandle!! }} + |
+ |${super.reviseMsg(task, request, response, formText) { formHandle!! }} + """.trimMargin(), className = "reply-message" + ) + formText.append(formHandle.toString()) + formHandle.toString() + task.complete() + } - fun acceptButton( - task: SessionTask, - request: CodingActor.CodeRequest, - response: CodingActor.CodeResult, - formText: StringBuilder, - formHandle: () -> StringBuilder - ): String { - return ui.hrefLink("\uD83D\uDC4D", "href-link play-button") { - genState.taskResult[taskId] = response.let { - """ - |## Shell Command Output - | - |``` - |${response.code} - |``` - | - |``` - |${response.renderedResponse} - |``` - """.trimMargin() - } - function() + fun acceptButton( + task: SessionTask, + request: CodingActor.CodeRequest, + response: CodingActor.CodeResult, + formText: StringBuilder, + formHandle: () -> StringBuilder + ): String { + return ui.hrefLink("Accept", "href-link play-button") { + genState.taskResult[taskId] = response.let { + """ + |## Shell Command Output + | + |``` + |${response.code} + |``` + | + |``` + |${response.renderedResponse} + |``` + """.trimMargin() + } + function() + } + } + }.apply { + start( + codeRequest( + listOf( + userMessage to Role.user, + highLevelPlan.text to Role.assistant, + priorCode to Role.assistant, + inputFileCode() to Role.assistant, + ) + ) + ) } - } - }.apply { - start( - codeRequest( - listOf( - userMessage to Role.user, - highLevelPlan.text to Role.assistant, - priorCode to Role.assistant, - inputFileCode to Role.assistant, - ) - ) - ) } - } - private fun createFiles( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: String, - subTask: Task, - genState: GenState, - taskId: String, - taskTabs: TabbedDisplay, - onComplete: () -> Unit - ) { + private fun createFiles( + task: SessionTask, + userMessage: String, + highLevelPlan: ParsedResponse, + priorCode: String, + inputFileCode: () -> String, + subTask: Task, + genState: GenState, + taskId: String, + taskTabs: TabbedDisplay, + onComplete: () -> Unit + ) { - val process = { sb: StringBuilder -> - val codeResult = newFileCreatorActor.answer( - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode, - subTask.description ?: "", - ).filter { it.isNotBlank() }, api - ) - genState.taskResult[taskId] = codeResult - renderMarkdown(ui.socketManager.addSaveLinks(codeResult, task) { path, newCode -> - val prev = codeFiles[path] - if (prev != newCode) { + val process = { sb: StringBuilder -> + val codeResult = newFileCreatorActor.answer( + listOf( + userMessage, + highLevelPlan.text, + priorCode, + inputFileCode(), + subTask.description ?: "", + ).filter { it.isNotBlank() }, api + ) + genState.taskResult[taskId] = codeResult + renderMarkdown(ui.socketManager.addSaveLinks(codeResult, task, ui = ui) { path, newCode -> + val prev = codeFiles[path] + if (prev != newCode) { // codeFiles[path] = newCode - val bytes = newCode.toByteArray(Charsets.UTF_8) - val saveFile = task.saveFile(path, bytes) - task.complete("$path Created") - } else { - task.complete("No changes to $path") + val bytes = newCode.toByteArray(Charsets.UTF_8) + val saveFile = task.saveFile(path, bytes) + task.complete("$path Created") + } else { + task.complete("No changes to $path") + } + }, ui = ui) + acceptButtonFooter(sb) { + taskTabs.selectedTab = taskTabs.selectedTab + 1 + taskTabs.update() + onComplete() + } + } + object : Retryable(ui, task, process) { + init { + set(label(size), process(container!!)) + } } - }, ui=ui) + accept(sb) { - taskTabs.selectedTab = taskTabs.selectedTab + 1 - taskTabs.update() - onComplete() - } - } - object : Retryable(ui, task, process) { - init { - set(label(size), process(container!!)) - } } - } - private fun editFiles( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: String, - subTask: Task, - genState: GenState, - taskId: String, - taskTabs: TabbedDisplay, - onComplete: () -> Unit, - ) { - val process = { sb: StringBuilder -> - val codeResult = filePatcherActor.answer( - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode, - subTask.description ?: "", - ).filter { it.isNotBlank() }, api - ) - genState.taskResult[taskId] = codeResult - renderMarkdown( - ui.socketManager.addApplyFileDiffLinks( - root = root, - ui = ui, - code = codeFiles, - response = codeResult, - handle = { newCodeMap -> - val codeFiles = codeFiles - newCodeMap.forEach { (path, newCode) -> - val prev = codeFiles[path] - if (prev != newCode) { + private fun editFiles( + task: SessionTask, + userMessage: String, + highLevelPlan: ParsedResponse, + priorCode: String, + inputFileCode: () -> String, + subTask: Task, + genState: GenState, + taskId: String, + taskTabs: TabbedDisplay, + onComplete: () -> Unit, + ) { + val process = { sb: StringBuilder -> + val codeResult = filePatcherActor.answer( + listOf( + userMessage, + highLevelPlan.text, + priorCode, + inputFileCode(), + subTask.description ?: "", + ).filter { it.isNotBlank() }, api + ) + genState.taskResult[taskId] = codeResult + renderMarkdown( + ui.socketManager.addApplyFileDiffLinks( + root = root, + code = codeFiles, + response = codeResult, + handle = { newCodeMap -> + val codeFiles = codeFiles + newCodeMap.forEach { (path, newCode) -> + val prev = codeFiles[path] + if (prev != newCode) { // codeFiles[path] = newCode - task.complete( - "$path Updated" - ) - } + task.complete( + "$path Updated" + ) + } + } + }, + ui = ui + ) + acceptButtonFooter(sb) { + taskTabs.selectedTab += 1 + taskTabs.update() + task.complete() + onComplete() + }, ui = ui + ) + } + object : Retryable(ui, task, process) { + init { + set(label(size), process(container!!)) } - }, task = task - ) + accept(sb) { - taskTabs.selectedTab += 1 - taskTabs.update() - task.complete() - onComplete() - }, ui=ui) - } - object : Retryable(ui, task, process) { - init { - set(label(size), process(container!!)) - } + } } - } - private fun document( - task: SessionTask, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: String, - genState: GenState, - taskId: String, - taskTabs: TabbedDisplay, - onComplete: () -> Unit - ) { - val process = { sb: StringBuilder -> - val docResult = documentationGeneratorActor.answer( - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode, - ).filter { it.isNotBlank() }, api - ) - genState.taskResult[taskId] = docResult - renderMarkdown("## Generated Documentation\n$docResult", ui=ui) + accept(sb) { - taskTabs.selectedTab = taskTabs.selectedTab + 1 - taskTabs.update() - task.complete() - onComplete() - } + private fun document( + task: SessionTask, + userMessage: String, + highLevelPlan: ParsedResponse, + priorCode: String, + inputFileCode: () -> String, + genState: GenState, + taskId: String, + taskTabs: TabbedDisplay, + onComplete: () -> Unit + ) { + val process = { sb: StringBuilder -> + val docResult = documentationGeneratorActor.answer( + listOf( + userMessage, + highLevelPlan.text, + priorCode, + inputFileCode(), + ).filter { it.isNotBlank() }, api + ) + genState.taskResult[taskId] = docResult + renderMarkdown("## Generated Documentation\n$docResult", ui = ui) + acceptButtonFooter(sb) { + taskTabs.selectedTab = taskTabs.selectedTab + 1 + taskTabs.update() + task.complete() + onComplete() + } + } + object : Retryable(ui, task, process) { + init { + set(label(size), process(container!!)) + } + } } - object : Retryable(ui, task, process) { - init { - set(label(size), process(container!!)) - } + + private fun acceptButtonFooter(stringBuilder: StringBuilder, fn: () -> Unit): String { + val footerTask = ui.newTask(false) + lateinit var textHandle: StringBuilder + textHandle = footerTask.complete(ui.hrefLink("Accept", classname = "href-link cmd-button") { + try { + textHandle.set("""
Accepted
""") + footerTask.complete() + } catch (e: Throwable) { + log.warn("Error", e) + } + fn() + })!! + return footerTask.placeholder } - } - private fun accept(stringBuilder: StringBuilder, fn: () -> Unit): String { - val startTag = """""" - val endTag = """""" - return startTag + ui.hrefLink("Accept") { - try { - val prev = stringBuilder.toString() - require(prev.contains(startTag) && prev.contains(endTag)) { - "Accept link not found" + private fun inquiry( + subTask: Task, + userMessage: String, + highLevelPlan: ParsedResponse, + priorCode: String, + inputFileCode: () -> String, + genState: GenState, + taskId: String, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + val input1 = "Expand ${subTask.description ?: ""}" + val toInput = { it: String -> + listOf( + userMessage, + highLevelPlan.text, + priorCode, + inputFileCode(), + it, + ).filter { it.isNotBlank() } } - val newValue = prev.substringBefore(startTag) + "Accepted" + prev.substringAfter(endTag) - stringBuilder.clear() - stringBuilder.append(newValue) - } catch (e: Throwable) { - log.warn("Error", e) - } - fn() - } + endTag - } - - private fun inquiry( - subTask: Task, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: String, - genState: GenState, - taskId: String, - task: SessionTask, - taskTabs: TabbedDisplay - ) { - val input1 = "Expand ${subTask.description ?: ""}" - val toInput = { it: String -> - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode, - it, - ).filter { it.isNotBlank() } + val inquiryResult = Acceptable( + task = task, + userMessage = "Expand ${subTask.description ?: ""}\n${toJson(subTask)}", + heading = "", + initialResponse = { it: String -> inquiryActor.answer(toInput(it), api = api) }, + outputFn = { design: String -> + renderMarkdown(design, ui = ui) + }, + ui = ui, + reviseResponse = { userMessages: List> -> + inquiryActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput("Expand ${subTask.description ?: ""}\n${toJson(subTask)}"), + api = api + ) + }, + atomicRef = AtomicReference(), + semaphore = Semaphore(0), + ).call() + genState.taskResult[taskId] = inquiryResult } - val inquiryResult = Acceptable( - task = task, - userMessage = "Expand ${subTask.description ?: ""}\n${toJson(subTask)}", - heading = "", - initialResponse = { it: String -> inquiryActor.answer(toInput(it), api = api) }, - outputFn = { design: String -> - renderMarkdown(design, ui=ui) - }, - ui = ui, - reviseResponse = { userMessages: List> -> - inquiryActor.respond( - messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } - .toTypedArray()), - input = toInput("Expand ${subTask.description ?: ""}\n${toJson(subTask)}"), - api = api - ) - }, - atomicRef = AtomicReference(), - semaphore = Semaphore(0), - ).call() - genState.taskResult[taskId] = inquiryResult - } - private fun taskPlanning( - subTask: Task, - userMessage: String, - highLevelPlan: ParsedResponse, - priorCode: String, - inputFileCode: String, - genState: GenState, - taskId: String, - task: SessionTask, - taskTabs: TabbedDisplay - ) { - val toInput = { it: String -> - listOf( - userMessage, - highLevelPlan.text, - priorCode, - inputFileCode, - it - ).filter { it.isNotBlank() } - } - val input1 = "Expand ${subTask.description ?: ""}\n${toJson(subTask)}" - val subPlan = Acceptable( - task = task, - userMessage = input1, - heading = "", - initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, - outputFn = { design: ParsedResponse -> - displayMapInTabs( - mapOf( - "Text" to renderMarkdown(design.text, ui=ui), - "JSON" to renderMarkdown("```json\n${toJson(design.obj)/*.indent(" ")*/}\n```", ui=ui), - ) - ) - }, - ui = ui, - reviseResponse = { userMessages: List> -> - taskBreakdownActor.respond( - messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } - .toTypedArray()), - input = toInput(input1), - api = api - ) - }, - ).call() - genState.taskResult[taskId] = subPlan.text - var newTasks = subPlan.obj.tasksByID - newTasks?.forEach { - val newTask = ui.newTask() - genState.uitaskMap[it.key] = newTask - genState.tasksByDescription[it.value.description] = it.value - taskTabs[it.value.description ?: it.key] = "
" - } - val conflictingKeys = newTasks?.keys?.intersect(genState.subTasks.keys) - newTasks = newTasks?.entries?.associate { (key, value) -> - (when { - conflictingKeys?.contains(key) == true -> "${taskId}_${key}" - else -> key - }) to value.copy(task_dependencies = value.task_dependencies?.map { key -> - when { - conflictingKeys?.contains(key) == true -> "${taskId}_${key}" - else -> key + private fun taskPlanning( + subTask: Task, + userMessage: String, + highLevelPlan: ParsedResponse, + priorCode: String, + inputFileCode: () -> String, + genState: GenState, + taskId: String, + task: SessionTask, + taskTabs: TabbedDisplay + ) { + val toInput = { it: String -> + listOf( + userMessage, + highLevelPlan.text, + priorCode, + inputFileCode(), + it + ).filter { it.isNotBlank() } } - }) - } - log.debug("New Tasks: ${newTasks?.keys}") - genState.subTasks.putAll(newTasks ?: emptyMap()) - executionOrder(newTasks ?: emptyMap()).reversed().forEach { genState.taskIdProcessingQueue.add(0, it) } - genState.subTasks.values.forEach { - it.task_dependencies = it.task_dependencies?.map { dep -> - when { - dep == taskId -> subPlan.obj.finalTaskID ?: dep - else -> dep + val input1 = "Expand ${subTask.description ?: ""}\n${toJson(subTask)}" + val subPlan = Acceptable( + task = task, + userMessage = input1, + heading = "", + initialResponse = { it: String -> taskBreakdownActor.answer(toInput(it), api = api) }, + outputFn = { design: ParsedResponse -> + displayMapInTabs( + mapOf( + "Text" to renderMarkdown(design.text, ui = ui), + "JSON" to renderMarkdown("```json\n${toJson(design.obj)/*.indent(" ")*/}\n```", ui = ui), + ) + ) + }, + ui = ui, + reviseResponse = { userMessages: List> -> + taskBreakdownActor.respond( + messages = (userMessages.map { ApiModel.ChatMessage(it.second, it.first.toContentList()) } + .toTypedArray()), + input = toInput(input1), + api = api + ) + }, + ).call() + genState.taskResult[taskId] = subPlan.text + var newTasks = subPlan.obj.tasksByID + newTasks?.forEach { + val newTask = ui.newTask() + genState.uitaskMap[it.key] = newTask + genState.tasksByDescription[it.value.description] = it.value + taskTabs[it.value.description ?: it.key] = "
" + } + val conflictingKeys = newTasks?.keys?.intersect(genState.subTasks.keys) + newTasks = newTasks?.entries?.associate { (key, value) -> + (when { + conflictingKeys?.contains(key) == true -> "${taskId}_${key}" + else -> key + }) to value.copy(task_dependencies = value.task_dependencies?.map { key -> + when { + conflictingKeys?.contains(key) == true -> "${taskId}_${key}" + else -> key + } + }) + } + log.debug("New Tasks: ${newTasks?.keys}") + genState.subTasks.putAll(newTasks ?: emptyMap()) + executionOrder(newTasks ?: emptyMap()).reversed().forEach { genState.taskIdProcessingQueue.add(0, it) } + genState.subTasks.values.forEach { + it.task_dependencies = it.task_dependencies?.map { dep -> + when { + dep == taskId -> subPlan.obj.finalTaskID ?: dep + else -> dep + } + } } - } } - } - private fun getAllDependencies(subTask: Task, subTasks: MutableMap): List { - return getAllDependenciesHelper(subTask, subTasks, mutableSetOf()) - } + private fun getAllDependencies(subTask: Task, subTasks: MutableMap): List { + return getAllDependenciesHelper(subTask, subTasks, mutableSetOf()) + } - private fun getAllDependenciesHelper( - subTask: Task, - subTasks: MutableMap, - visited: MutableSet - ): List { - val dependencies = subTask.task_dependencies?.toMutableList() ?: mutableListOf() - subTask.task_dependencies?.forEach { dep -> - if (dep in visited) return@forEach - val subTask = subTasks[dep] - if (subTask != null) { - visited.add(dep) - dependencies.addAll(getAllDependenciesHelper(subTask, subTasks, visited)) - } + private fun getAllDependenciesHelper( + subTask: Task, + subTasks: MutableMap, + visited: MutableSet + ): List { + val dependencies = subTask.task_dependencies?.toMutableList() ?: mutableListOf() + subTask.task_dependencies?.forEach { dep -> + if (dep in visited) return@forEach + val subTask = subTasks[dep] + if (subTask != null) { + visited.add(dep) + dependencies.addAll(getAllDependenciesHelper(subTask, subTasks, visited)) + } + } + return dependencies } - return dependencies - } - private fun buildMermaidGraph(subTasks: Map): String { - val graphBuilder = StringBuilder("graph TD;\n") - subTasks.forEach { (taskId, task) -> - val sanitizedTaskId = sanitizeForMermaid(taskId) - val taskType = task.taskType?.name ?: "Unknown" - val escapedDescription = escapeMermaidCharacters(task.description ?: "") - graphBuilder.append(" ${sanitizedTaskId}[$escapedDescription]:::$taskType;\n") - task.task_dependencies?.forEach { dependency -> - val sanitizedDependency = sanitizeForMermaid(dependency) - graphBuilder.append(" ${sanitizedDependency} --> ${sanitizedTaskId};\n") - } + private fun buildMermaidGraph(subTasks: Map): String { + val graphBuilder = StringBuilder("graph TD;\n") + subTasks.forEach { (taskId, task) -> + val sanitizedTaskId = sanitizeForMermaid(taskId) + val taskType = task.taskType?.name ?: "Unknown" + val escapedDescription = escapeMermaidCharacters(task.description ?: "") + graphBuilder.append(" ${sanitizedTaskId}[$escapedDescription]:::$taskType;\n") + task.task_dependencies?.forEach { dependency -> + val sanitizedDependency = sanitizeForMermaid(dependency) + graphBuilder.append(" ${sanitizedDependency} --> ${sanitizedTaskId};\n") + } + } + graphBuilder.append(" classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef NewFile fill:lightblue,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef EditFile fill:lightgreen,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef Documentation fill:lightyellow,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef Inquiry fill:orange,stroke:#333,stroke-width:2px;\n") + graphBuilder.append(" classDef TaskPlanning fill:lightgrey,stroke:#333,stroke-width:2px;\n") + return graphBuilder.toString() } - graphBuilder.append(" classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef NewFile fill:lightblue,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef EditFile fill:lightgreen,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef Documentation fill:lightyellow,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef Inquiry fill:orange,stroke:#333,stroke-width:2px;\n") - graphBuilder.append(" classDef TaskPlanning fill:lightgrey,stroke:#333,stroke-width:2px;\n") - return graphBuilder.toString() - } - private fun sanitizeForMermaid(input: String): String { - return input.replace(" ", "_") - .replace("\"", "\\\"") - .replace("[", "\\[") - .replace("]", "\\]") - .replace("(", "\\(") - .replace(")", "\\)") - } + private fun sanitizeForMermaid(input: String) = input + .replace(" ", "_") + .replace("\"", "\\\"") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("(", "\\(") + .replace(")", "\\)") + .let { "`$it`" } - private fun escapeMermaidCharacters(input: String): String { - return input - } + private fun escapeMermaidCharacters(input: String) = input + .replace("\"", "\\\"") + .let { '"' + it + '"' } - companion object { - private val log = LoggerFactory.getLogger(TaskRunnerAgent::class.java) + companion object { + private val log = LoggerFactory.getLogger(TaskRunnerAgent::class.java) - enum class ActorTypes { - TaskBreakdown, - DocumentationGenerator, - NewFileCreator, - FilePatcher, - Inquiry, - RunShellCommand, - } + enum class ActorTypes { + TaskBreakdown, + DocumentationGenerator, + NewFileCreator, + FilePatcher, + Inquiry, + RunShellCommand, + } - fun executionOrder(tasks: Map): List { - val taskIds: MutableList = mutableListOf() - val taskMap = tasks.toMutableMap() - while (taskMap.isNotEmpty()) { - val nextTasks = taskMap.filter { (_, task) -> task.task_dependencies?.all { taskIds.contains(it) } ?: true } - if (nextTasks.isEmpty()) { - throw RuntimeException("Circular dependency detected in task breakdown") + fun executionOrder(tasks: Map): List { + val taskIds: MutableList = mutableListOf() + val taskMap = tasks.toMutableMap() + while (taskMap.isNotEmpty()) { + val nextTasks = + taskMap.filter { (_, task) -> task.task_dependencies?.all { taskIds.contains(it) } ?: true } + if (nextTasks.isEmpty()) { + throw RuntimeException("Circular dependency detected in task breakdown") + } + taskIds.addAll(nextTasks.keys) + nextTasks.keys.forEach { taskMap.remove(it) } + } + return taskIds } - taskIds.addAll(nextTasks.keys) - nextTasks.keys.forEach { taskMap.remove(it) } - } - return taskIds - } - val isWindows = System.getProperty("os.name").lowercase(Locale.getDefault()).contains("windows") - } + val isWindows = System.getProperty("os.name").lowercase(Locale.getDefault()).contains("windows") + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt index 49732a68..bfc75831 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt @@ -4,7 +4,7 @@ import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.actions.dev.AppServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools -import com.github.simiacryptus.aicoder.util.addApplyFileDiffLinks +import com.github.simiacryptus.diff.addApplyFileDiffLinks import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.vfs.VirtualFile import com.simiacryptus.jopenai.API @@ -37,105 +37,106 @@ val VirtualFile.toFile: File get() = File(this.path) class WebDevAction : BaseAction() { - val path = "/webDev" + val path = "/webDev" - override fun handle(e: AnActionEvent) { - val session = StorageInterface.newGlobalID() - val storage = ApplicationServices.dataStorageFactory(DiffChatAction.root) as DataStorage? - val selectedFile = UITools.getSelectedFolder(e) - if (null != storage && null != selectedFile) { - DataStorage.sessionPaths[session] = selectedFile.toFile + override fun handle(e: AnActionEvent) { + val session = StorageInterface.newGlobalID() + val storage = ApplicationServices.dataStorageFactory(DiffChatAction.root) as DataStorage? + val selectedFile = UITools.getSelectedFolder(e) + if (null != storage && null != selectedFile) { + DataStorage.sessionPaths[session] = selectedFile.toFile + } + agents[session] = WebDevApp() + 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")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() } - agents[session] = WebDevApp() - 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")) - } 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) - } + override fun isEnabled(event: AnActionEvent): Boolean { + if (UITools.getSelectedFile(event)?.isDirectory == false) return false + return super.isEnabled(event) + } - open class WebDevApp( - applicationName: String = "Web Dev Assistant v1.1", - open val symbols: Map = mapOf(), - val temperature: Double = 0.1, - ) : ApplicationServer( - applicationName = applicationName, - path = "/webdev", - ) { - override fun userMessage( - session: Session, - user: User?, - userMessage: String, - ui: ApplicationInterface, - api: API + open class WebDevApp( + applicationName: String = "Web Dev Assistant v1.1", + open val symbols: Map = mapOf(), + val temperature: Double = 0.1, + ) : ApplicationServer( + applicationName = applicationName, + path = "/webdev", + showMenubar = false, ) { - 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, - ).start( - userMessage = userMessage, - ) - } + 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, + ).start( + userMessage = userMessage, + ) + } - data class Settings( - val budget: Double? = 2.00, - val tools: List = emptyList(), - val model: ChatModels = ChatModels.GPT4Turbo, - ) + data class Settings( + val budget: Double? = 2.00, + val tools: List = emptyList(), + val model: ChatModels = ChatModels.GPT4Turbo, + ) - override val settingsClass: Class<*> get() = Settings::class.java + override val settingsClass: Class<*> get() = Settings::class.java - @Suppress("UNCHECKED_CAST") - override fun initSettings(session: Session): T? = Settings() as T - } + @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 tools: List = emptyList(), - private val actorMap: Map> = mapOf( - ActorTypes.HtmlCodingActor to SimpleActor( - prompt = """ + class WebDevAgent( + val api: API, + dataStorage: StorageInterface, + session: Session, + user: User?, + val ui: ApplicationInterface, + val model: ChatModels, + val tools: List = emptyList(), + private val actorMap: Map> = mapOf( + ActorTypes.HtmlCodingActor to SimpleActor( + prompt = """ 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.JavascriptCodingActor to SimpleActor( - prompt = """ + ), + 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 = """ + ), + ActorTypes.CssCodingActor to SimpleActor( + prompt = """ You will translate the user request into a CSS file for use in a rich javascript application. """.trimIndent(), model = model - ), - ActorTypes.ArchitectureDiscussionActor to ParsedActor( - resultClass = PageResourceList::class.java, - prompt = """ + ), + ActorTypes.ArchitectureDiscussionActor to ParsedActor( + resultClass = PageResourceList::class.java, + prompt = """ Translate the user's idea into a detailed architecture for a simple web application. Suggest specific frameworks/libraries to import and provide CDN links for them. Specify user interactions and how the application will respond to them. @@ -143,11 +144,11 @@ class WebDevAction : BaseAction() { Identify coding styles and patterns to be used. List all files to be created, and for each file, describe the public interface / purpose / content summary. """.trimIndent(), - model = model, - parsingModel = model, - ), - ActorTypes.CodeReviewer to SimpleActor( - prompt = """ + model = model, + parsingModel = model, + ), + 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. @@ -169,359 +170,381 @@ class WebDevAction : BaseAction() { Continued text """.trimIndent(), - model = model, - ), - ActorTypes.EtcCodingActor to SimpleActor( - prompt = """ + 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 - ), - ), - ) : - ActorSystem(actorMap.map { it.key.name to it.value }.toMap(), dataStorage, user, session) { - enum class ActorTypes { - HtmlCodingActor, - JavascriptCodingActor, - CssCodingActor, - ArchitectureDiscussionActor, - CodeReviewer, - EtcCodingActor, - } - - private val architectureDiscussionActor by lazy { getActor(ActorTypes.ArchitectureDiscussionActor) as ParsedActor } - private val htmlActor by lazy { getActor(ActorTypes.HtmlCodingActor) as SimpleActor } - private val javascriptActor by lazy { getActor(ActorTypes.JavascriptCodingActor) 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 } + model = model + ), + ), + ) : + ActorSystem( + actorMap.map { it.key.name to it.value }.toMap(), + dataStorage, + user, + session + ) { + enum class ActorTypes { + HtmlCodingActor, + JavascriptCodingActor, + CssCodingActor, + ArchitectureDiscussionActor, + CodeReviewer, + EtcCodingActor, + } - private val codeFiles = mutableMapOf() + private val architectureDiscussionActor by lazy { getActor(ActorTypes.ArchitectureDiscussionActor) as ParsedActor } + private val htmlActor by lazy { getActor(ActorTypes.HtmlCodingActor) as SimpleActor } + private val javascriptActor by lazy { getActor(ActorTypes.JavascriptCodingActor) 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 } - fun start( - userMessage: String, - ) { - val task = ui.newTask() - val toInput = { it: String -> listOf(it) } - val architectureResponse = Acceptable( - task = task, - userMessage = userMessage, - initialResponse = { it: String -> architectureDiscussionActor.answer(toInput(it), api = api) }, - outputFn = { design: ParsedResponse -> - // renderMarkdown("${design.text}\n\n```json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\n```") - AgentPatterns.displayMapInTabs( - mapOf( - "Text" to renderMarkdown(design.text, ui=ui), - "JSON" to renderMarkdown("```json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\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() + private val codeFiles = mutableMapOf() - try { - val toolSpecs = tools.map { ToolServlet.tools.find { t -> t.path == it } } - .joinToString("\n\n") { it?.let { JsonUtil.toJson(it.openApiDescription) } ?: "" } - var messageWithTools = userMessage - if (toolSpecs.isNotBlank()) messageWithTools += "\n\nThese services are available:\n$toolSpecs" - task.echo(renderMarkdown("```json\n${JsonUtil.toJson(architectureResponse.obj)/*.indent(" ")*/}\n```", ui=ui)) - architectureResponse.obj.resources.filter { - !it.path!!.startsWith("http") - }.forEach { (path, description) -> - val task = ui.newTask() - when (path!!.split(".").last().lowercase()) { + fun start( + userMessage: String, + ) { + val task = ui.newTask() + val toInput = { it: String -> listOf(it) } + val architectureResponse = Acceptable( + task = task, + userMessage = userMessage, + initialResponse = { it: String -> architectureDiscussionActor.answer(toInput(it), api = api) }, + outputFn = { design: ParsedResponse -> + // renderMarkdown("${design.text}\n\n```json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\n```") + AgentPatterns.displayMapInTabs( + mapOf( + "Text" to renderMarkdown(design.text, ui = ui), + "JSON" to renderMarkdown( + "```json\n${JsonUtil.toJson(design.obj)/*.indent(" ")*/}\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() - "js" -> draftResourceCode( - task, - javascriptActor.chatMessages( - listOf( - messageWithTools, - architectureResponse.text, - "Render $path - $description" + try { + val toolSpecs = tools.map { ToolServlet.tools.find { t -> t.path == it } } + .joinToString("\n\n") { it?.let { JsonUtil.toJson(it.openApiDescription) } ?: "" } + var messageWithTools = userMessage + if (toolSpecs.isNotBlank()) messageWithTools += "\n\nThese services are available:\n$toolSpecs" + task.echo( + renderMarkdown( + "```json\n${JsonUtil.toJson(architectureResponse.obj)/*.indent(" ")*/}\n```", + ui = ui + ) ) - ), - javascriptActor, - path!!, "js", "javascript" - ) + architectureResponse.obj.resources.filter { + !it.path!!.startsWith("http") + }.forEach { (path, description) -> + val task = ui.newTask() + when (path!!.split(".").last().lowercase()) { + "js" -> draftResourceCode( + task, + javascriptActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + javascriptActor, + path!!, "js", "javascript" + ) - "css" -> draftResourceCode( - task, - cssActor.chatMessages( - listOf( - messageWithTools, - architectureResponse.text, - "Render $path - $description" - ) - ), - cssActor, - path - ) - "html" -> draftResourceCode( - task, - htmlActor.chatMessages( - listOf( - messageWithTools, - architectureResponse.text, - "Render $path - $description" - ) - ), - htmlActor, - path - ) + "css" -> draftResourceCode( + task, + cssActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + cssActor, + path + ) - else -> draftResourceCode( - task, - etcActor.chatMessages( - listOf( - messageWithTools, - architectureResponse.text, - "Render $path - $description" - ) - ), - etcActor, path - ) + "html" -> draftResourceCode( + task, + htmlActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + htmlActor, + path + ) - } - } - // Apply codeReviewer - fun codeSummary() = codeFiles.entries.joinToString("\n\n") { (path, code) -> - "# $path\n```${ - path.split('.').last()?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } - }\n${code?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }}\n```" - } + else -> draftResourceCode( + task, + etcActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + etcActor, path + ) + } + } + // Apply codeReviewer + fun codeSummary() = codeFiles.entries.joinToString("\n\n") { (path, code) -> + "# $path\n```${ + path.split('.').last()?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + }\n${code?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }}\n```" + } - fun outputFn(task: SessionTask, design: String): StringBuilder? { - //val task = ui.newTask() - return task.complete( - ui.socketManager.addApplyFileDiffLinks( - root = codeFiles.keys.map { File(it).toPath() }.toTypedArray().commonRoot(), - code = codeFiles, - response = design, - handle = { newCodeMap -> - newCodeMap.forEach { (path, newCode) -> - val prev = codeFiles[path] - if (prev != newCode) { - codeFiles[path] = newCode - task.complete( - "$path Updated" + ) + } + } + }, + ui = ui ) - }'>$path Updated" ) - } } - }, - task = task, - ui = ui - ) - ) - } - try { - var task = ui.newTask() - task.add(message = renderMarkdown(codeSummary(), ui=ui)) - var design = codeReviewer.answer(listOf(element = codeSummary()), api = api) - outputFn(task, design) - var textInputHandle: StringBuilder? = null - var textInput: String? = null - val feedbackGuard = AtomicBoolean(false) - textInput = ui.textInput { userResponse -> - if (feedbackGuard.getAndSet(true)) return@textInput - textInputHandle?.clear() - task.complete() - task = ui.newTask() - task.echo(renderMarkdown(userResponse, ui=ui)) - val codeSummary = codeSummary() - task.add(renderMarkdown(codeSummary, ui=ui)) - design = codeReviewer.respond( - messages = codeReviewer.chatMessages( - listOf( - codeSummary, - userResponse, - ) - ), - input = listOf(element = codeSummary), - api = api - ) - outputFn(task, design) - textInputHandle = task.complete(textInput!!) - feedbackGuard.set(false) - } - textInputHandle = task.complete(textInput) - } catch (e: Throwable) { - val task = ui.newTask() - task.error(ui = ui, e = e) - throw e + try { + var task = ui.newTask() + task.add(message = renderMarkdown(codeSummary(), ui = ui)) + var design = codeReviewer.answer(listOf(element = codeSummary()), api = api) + outputFn(task, design) + var textInputHandle: StringBuilder? = null + var textInput: String? = null + val feedbackGuard = AtomicBoolean(false) + textInput = ui.textInput { userResponse -> + if (feedbackGuard.getAndSet(true)) return@textInput + textInputHandle?.clear() + task.complete() + task = ui.newTask() + task.echo(renderMarkdown(userResponse, ui = ui)) + val codeSummary = codeSummary() + task.add(renderMarkdown(codeSummary, ui = ui)) + design = codeReviewer.respond( + messages = codeReviewer.chatMessages( + listOf( + codeSummary, + userResponse, + ) + ), + input = listOf(element = codeSummary), + api = api + ) + outputFn(task, design) + textInputHandle = task.complete(textInput!!) + feedbackGuard.set(false) + } + textInputHandle = task.complete(textInput) + } catch (e: Throwable) { + val task = ui.newTask() + task.error(ui = ui, e = e) + throw e + } + } catch (e: Throwable) { + log.warn("Error", e) + task.error(ui, e) + } } - } catch (e: Throwable) { - log.warn("Error", e) - task.error(ui, e) - } - } - private fun draftResourceCode( - task: SessionTask, - request: Array, - actor: SimpleActor, - path: String, - vararg languages: String = arrayOf(path.split(".").last().lowercase()), - ) { - try { - var code = actor.respond(emptyList(), api, *request) - languages.forEach { language -> - if (code.contains("```$language")) code = code.substringAfter("```$language").substringBefore("```") - } - try { - task.add(renderMarkdown("```${languages.first()}\n${code?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }}\n```", ui=ui)) - task.add("$path Updated") - codeFiles[path] = code - val request1 = (request.toList() + - listOf( - ApiModel.ChatMessage(ApiModel.Role.assistant, code.toContentList()), - )).toTypedArray() - val formText = StringBuilder() - var formHandle: StringBuilder? = null - formHandle = task.add( - """ + private fun draftResourceCode( + task: SessionTask, + request: Array, + actor: SimpleActor, + path: String, + vararg languages: String = arrayOf(path.split(".").last().lowercase()), + ) { + try { + var code = actor.respond(emptyList(), api, *request) + languages.forEach { language -> + if (code.contains("```$language")) code = code.substringAfter("```$language").substringBefore("```") + } + try { + task.add( + renderMarkdown( + "```${languages.first()}\n${code?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }}\n```", + ui = ui + ) + ) + task.add("$path Updated") + codeFiles[path] = code + val request1 = (request.toList() + + listOf( + ApiModel.ChatMessage(ApiModel.Role.assistant, code.toContentList()), + )).toTypedArray() + val formText = StringBuilder() + var formHandle: StringBuilder? = null + formHandle = task.add( + """ |
|${ - ui.hrefLink("♻", "href-link regen-button") { - val task = ui.newTask() - responseAction(task, "Regenerating...", formHandle!!, formText) { - draftResourceCode( - task, - request1.dropLastWhile { it.role == ApiModel.Role.assistant }.toTypedArray(), - actor, path, *languages - ) - } - } - } + ui.hrefLink("♻", "href-link regen-button") { + val task = ui.newTask() + responseAction(task, "Regenerating...", formHandle!!, formText) { + draftResourceCode( + task, + request1.dropLastWhile { it.role == ApiModel.Role.assistant } + .toTypedArray(), + actor, path, *languages + ) + } + } + } |
|${ - ui.textInput { feedback -> - responseAction(task, "Revising...", formHandle!!, formText) { - //val task = ui.newTask() - try { - task.echo(renderMarkdown(feedback, ui=ui)) - draftResourceCode( - task, (request1.toList() + listOf( - code to ApiModel.Role.assistant, - feedback to ApiModel.Role.user, - ).filter { it.first.isNotBlank() } - .map { - ApiModel.ChatMessage( - it.second, - it.first.toContentList() - ) - }).toTypedArray(), actor, path, *languages + ui.textInput { feedback -> + responseAction(task, "Revising...", formHandle!!, formText) { + //val task = ui.newTask() + try { + task.echo(renderMarkdown(feedback, ui = ui)) + draftResourceCode( + task, (request1.toList() + listOf( + code to ApiModel.Role.assistant, + feedback to ApiModel.Role.user, + ).filter { it.first.isNotBlank() } + .map { + ApiModel.ChatMessage( + it.second, + it.first.toContentList() + ) + }).toTypedArray(), actor, path, *languages + ) + } catch (e: Throwable) { + log.warn("Error", e) + task.error(ui, e) + } + } + } + } + """.trimMargin(), className = "reply-message" ) - } catch (e: Throwable) { - log.warn("Error", e) + formText.append(formHandle.toString()) + formHandle.toString() + task.complete() + } catch (e: Throwable) { task.error(ui, e) - } + log.warn("Error", e) } - } + } catch (e: Throwable) { + log.warn("Error", e) + val error = task.error(ui, e) + var regenButton: StringBuilder? = null + regenButton = task.complete(ui.hrefLink("♻", "href-link regen-button") { + regenButton?.clear() + val header = task.header("Regenerating...") + draftResourceCode(task, request, actor, path, *languages) + header?.clear() + error?.clear() + task.complete() + }) } - """.trimMargin(), className = "reply-message" - ) - formText.append(formHandle.toString()) - formHandle.toString() - task.complete() - } catch (e: Throwable) { - task.error(ui, e) - log.warn("Error", e) } - } catch (e: Throwable) { - log.warn("Error", e) - val error = task.error(ui, e) - var regenButton: StringBuilder? = null - regenButton = task.complete(ui.hrefLink("♻", "href-link regen-button") { - regenButton?.clear() - val header = task.header("Regenerating...") - draftResourceCode(task, request, actor, path, *languages) - header?.clear() - error?.clear() - task.complete() - }) - } - } - private fun responseAction( - task: SessionTask, - message: String, - formHandle: StringBuilder?, - formText: StringBuilder, - fn: () -> Unit = {} - ) { - formHandle?.clear() - val header = task.header(message) - try { - fn() - } finally { - header?.clear() - var revertButton: StringBuilder? = null - revertButton = task.complete(ui.hrefLink("↩", "href-link regen-button") { - revertButton?.clear() - formHandle?.append(formText) - task.complete() - }) - } + private fun responseAction( + task: SessionTask, + message: String, + formHandle: StringBuilder?, + formText: StringBuilder, + fn: () -> Unit = {} + ) { + formHandle?.clear() + val header = task.header(message) + try { + fn() + } finally { + header?.clear() + var revertButton: StringBuilder? = null + revertButton = task.complete(ui.hrefLink("↩", "href-link regen-button") { + revertButton?.clear() + formHandle?.append(formText) + task.complete() + }) + } + } } - } - companion object { - private val log = LoggerFactory.getLogger(WebDevAction::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) { - 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 - } + companion object { + private val log = LoggerFactory.getLogger(WebDevAction::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 = 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 PageResourceList( - @Description("List of resources in this project; don't forget the index.html file!") - val resources: List = emptyList() - ) : ValidatedObject { - override fun validate(): String? = when { - resources.isEmpty() -> "Resources are required" - resources.any { it.validate() != null } -> "Invalid resource" - else -> null - } - } + data class PageResourceList( + @Description("List of resources in this project; don't forget the index.html file!") + val resources: List = emptyList() + ) : ValidatedObject { + override fun validate(): String? = when { + resources.isEmpty() -> "Resources are required" + resources.any { it.validate() != null } -> "Invalid resource" + else -> null + } + } - data class PageResource( - val path: String? = "", - val description: String? = "" - ) : ValidatedObject { - override fun validate(): String? = when { - path.isNullOrBlank() -> "Path is required" - path.contains(" ") -> "Path cannot contain spaces" - !path.contains(".") -> "Path must contain a file extension" - else -> null - } - } + data class PageResource( + val path: String? = "", + val description: String? = "" + ) : ValidatedObject { + override fun validate(): String? = when { + path.isNullOrBlank() -> "Path is required" + path.contains(" ") -> "Path cannot contain spaces" + !path.contains(".") -> "Path must contain a file extension" + else -> null + } + } - } + } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/PrintGitCommitPatchAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/PrintGitCommitPatchAction.kt new file mode 100644 index 00000000..69f1b2e7 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/PrintGitCommitPatchAction.kt @@ -0,0 +1,41 @@ +package com.github.simiacryptus.aicoder.actions.git + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.vcs.VcsDataKeys +import com.intellij.openapi.vcs.changes.Change +import com.intellij.openapi.vcs.changes.ChangeListManager +import com.intellij.openapi.vcs.history.VcsRevisionNumber + +class PrintGitCommitPatchAction : AnAction("Print Git Commit Patch") { + + override fun actionPerformed(event: AnActionEvent) { + val project: Project? = event.getData(CommonDataKeys.PROJECT) + val revision: VcsRevisionNumber? = event.getData(VcsDataKeys.VCS_REVISION_NUMBER) + val filePaths = event.getData(VcsDataKeys.CHANGES) ?: return + + if (project == null || revision == null) { + Messages.showErrorDialog(project, "No commit selected.", "Error") + return + } + + val changes = filePaths.mapNotNull { it as? Change } + val patches = changes.mapNotNull { change -> + val changeListManager = ChangeListManager.getInstance(project) + changeListManager.getChangeList(change)?.changes?.firstOrNull()?.beforeRevision?.content + } + + val patchText = patches.joinToString(separator = "\n\n", prefix = "Patch for Revision: $revision\n\n") + Messages.showInfoMessage(project, patchText, "Git Commit Patch") + } + + override fun update(event: AnActionEvent) { + // Enable action only if a project is open and a revision is selected + val project: Project? = event.getData(CommonDataKeys.PROJECT) + val revision: VcsRevisionNumber? = event.getData(VcsDataKeys.VCS_REVISION_NUMBER) + event.presentation.isEnabledAndVisible = project != null && revision != null + } +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt deleted file mode 100644 index dc2982a2..00000000 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt +++ /dev/null @@ -1,273 +0,0 @@ -package com.github.simiacryptus.aicoder.config - -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import com.github.simiacryptus.aicoder.ui.EditorMenu -import com.github.simiacryptus.aicoder.util.IdeaKotlinInterpreter -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.actionSystem.AnAction -import java.io.File - -class ActionSettingsRegistry( - val configDir: File -) { - - // Removed in-memory actionSettings map - private val version = 2.0080 // Increment this value to force a reload of all actions - - private val objectMapper = jacksonObjectMapper() - - private fun updateActionConfig(action: AnAction, code: String, language: String) { - var actionConfig = this.getActionConfig(action) - log.info("Updating action config for action: ${action.javaClass.name}, language: $language") - log.debug("Action Config before update: $actionConfig") - actionConfig.language = language - actionConfig.isDynamic = false - with(action) { - templatePresentation.text = actionConfig.displayText - templatePresentation.description = actionConfig.displayText - } - log.debug("Action Config after basic update: $actionConfig") - if (!actionConfig.enabled) { - log.info("Action ${action.javaClass.name} is disabled, skipping further updates") - return - } else if (!actionConfig.file.exists() - || actionConfig.file.readText().isBlank() - || (actionConfig.version ?: 0.0) < version - ) { - log.info("Writing new code to action file for ${action.javaClass.name}") - actionConfig.file.writeText(code) - actionConfig.version = version - saveActionConfig(actionConfig) // Save updated config to JSON file - } else { - handleDynamicActionConfig(action, actionConfig, code) - } - } - - private fun saveActionConfig(actionConfig: ActionSettings) { - val configFile = File(configDir, "${actionConfig.packageName.replace('.', '/')}/${actionConfig.className}.json") - configFile.parentFile.mkdirs() - objectMapper.writeValue(configFile, actionConfig) - log.info("Action config saved to ${configFile.path}") - } - - private fun handleDynamicActionConfig(action: AnAction, actionConfig: ActionSettings, code: String) { - log.debug("Entering handleDynamicActionConfig for ${action.javaClass.name}") - if (actionConfig.isDynamic || (actionConfig.version ?: 0.0) >= version) { - val localCode = actionConfig.file.readText().dropWhile { !it.isLetter() } - log.debug("Local code read from file for ${action.javaClass.name}") - if (!localCode.equals(code)) { - try { - log.info("Handling dynamic action config for action: ${action.javaClass.name}") - log.debug("Dynamic code differs from existing, updating for ${action.javaClass.name}") - val element = actionConfig.buildAction(localCode) - actionConfig.version = version - actionConfig.file.writeText(code) - throw ReplaceActionException() - } catch (e: Throwable) { - log.info("Error loading dynamic ${action.javaClass}", e) - log.error("Exception during dynamic action config handling for ${action.javaClass.name}", e) - } - } - } - val canLoad = try { - ActionSettingsRegistry::class.java.classLoader.loadClass(actionConfig.id) - log.debug("Successfully loaded class for action: ${actionConfig.id}") - true - } catch (e: Throwable) { - log.error("Failed to load class for action: ${actionConfig.id}", e) - false - } - if (canLoad) { - log.info("Can load class for action: ${actionConfig.id}, updating code.") - actionConfig.file.writeText(code) - actionConfig.version = version - } else { - log.info("Cannot load class for action: ${actionConfig.id}, removing action.") - throw RemoveActionException() - } - } - - fun edit(superChildren: Array): Array { - log.info("Starting edit process for actions") - val children = superChildren.toList().toMutableList() - children.toTypedArray().forEach { action -> - val language = "kt" - log.info("Editing action: ${action.javaClass.name}, language: $language") - log.debug("Attempting to load code for action: ${action.javaClass.name}") - val code: String? = load(action.javaClass, language) - if (null != code) { - try { - updateActionConfig(action, code, language) - } catch (e: RemoveActionException) { - children.remove(action) - log.info("Action removed due to RemoveActionException: ${action.javaClass.name}") - if (e.newAction != null) children.add(e.newAction) - } catch (e: Throwable) { - UITools.error(log, "Error loading ${action.javaClass}", e) - log.error("Exception caught during action editing: ${action.javaClass.name}", e) - } - } - } - this.getDynamicActions().forEach { - try { - if (!it.file.exists()) return@forEach - log.info("Adding dynamic action: ${it.id}") - if (!it.enabled) return@forEach - log.debug("Building dynamic action: ${it.id}") - val element = it.buildAction(it.file.readText()) - children.add(element) - } catch (e: Throwable) { - UITools.error(log, "Error loading dynamic action", e) - log.error("Exception caught during dynamic action addition: ${it.id}", e) - } - } - log.info("Edit process completed. Total actions: ${children.size}") - return children.toTypedArray() - } - - class ReplaceActionException : Exception() - class RemoveActionException(val newAction: AnAction? = null) : Exception() - - class DynamicActionException( - cause: Throwable, - msg: String, - val file: File, - val actionSetting: ActionSettings - ) : Exception(msg, cause) - - data class ActionSettings( - val configDir: File, - val id: String, // Static property - var enabled: Boolean = true, // User settable - // Adding logging within the buildAction method to log the action building process - var displayText: String? = null, // User settable - var version: Double? = null, // System property - var isDynamic: Boolean = false, // Static property - var language: String? = null, // Static property - val packageName: String = id.substringBeforeLast('.'), - val className: String = id.substringAfterLast('.'), - val file: File = File(configDir, "${packageName.replace('.', '/')}/$className.${language ?: "kt"}").apply { - parentFile.mkdirs() - }, - ) { - - fun buildAction( - code: String - ): AnAction = try { - val newClassName = this.className + "_" + Integer.toHexString(code.hashCode()) - with( - actionCache.getOrPut("$packageName.$newClassName") { - log.info("Compiling code for new action: $newClassName") - val code1 = code.replace( - ("""(? { - try { - val kotlinInterpreter = IdeaKotlinInterpreter(mapOf()) - val scriptEngine = kotlinInterpreter.scriptEngine - log.info("Compiling Kotlin code for action: ${this.id}") - val eval = scriptEngine.eval(code) - return eval as Class<*> - } catch (e: Throwable) { - throw DynamicActionException(e, "Error in Action " + displayText, file, this) - } - } - - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ActionSettings - - if (id != other.id) return false - if (isDynamic != other.isDynamic) return false - return language == other.language - } - - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + isDynamic.hashCode() - result = 31 * result + (language?.hashCode() ?: 0) - return result - } - - } - - private fun getActionConfig(action: AnAction): ActionSettings { - val configFile = File(configDir, "${action.javaClass.`package`.name.replace('.', '/')}/${action.javaClass.simpleName}.json") - return if (configFile.exists()) { - objectMapper.readValue(configFile) - } else { - val actionConfig = ActionSettings(configDir, action.javaClass.name) - log.info("Creating new action config for action: ${action.javaClass.name}") - actionConfig.displayText = action.templatePresentation.text - saveActionConfig(actionConfig) // Save new config to JSON file - actionConfig - } - } - - @JsonIgnore - fun getDynamicActions(): List { - val dynamicActionConfigs = configDir.walk().filter { it.extension == "json" }.map { objectMapper.readValue(it) } - return dynamicActionConfigs.filter { it.isDynamic && it.enabled }.toList() - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - // Removed comparison based on in-memory map - return true - } - - override fun hashCode(): Int { - // Removed hash code based on in-memory map - return super.hashCode() - } - - val actionSettings: Map - get() { - val allActionSettings = mutableMapOf() - configDir.walk().filter { it.extension == "json" }.forEach { file -> - val actionSetting = objectMapper.readValue(file) - allActionSettings[actionSetting.id] = actionSetting - } - return allActionSettings.toMap() - } - - companion object { - - private val log = org.slf4j.LoggerFactory.getLogger(ActionSettingsRegistry::class.java) - - private val actionCache = HashMap() - private fun load(actionPackage: String, actionName: String, language: String) = - load("/sources/${language}/$actionPackage/$actionName.$language") - - private fun load(path: String): String? { - val bytes = EditorMenu::class.java.getResourceAsStream(path)?.readAllBytes() - return bytes?.toString(Charsets.UTF_8)?.dropWhile { !it.isLetter() } - } - - fun load(clazz: Class, language: String) = - load(clazz.`package`.name.replace('.', '/'), clazz.simpleName, language) - - - } - -} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt deleted file mode 100644 index 91d75a9e..00000000 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.github.simiacryptus.aicoder.config - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.ui.VerticalFlowLayout -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.ui.BooleanTableCellEditor -import com.intellij.ui.BooleanTableCellRenderer -import com.intellij.ui.components.JBScrollPane -import com.intellij.ui.components.panels.HorizontalLayout -import com.intellij.ui.table.JBTable -import org.jdesktop.swingx.JXTable -import org.slf4j.LoggerFactory -import java.awt.BorderLayout -import java.awt.event.ActionEvent -import java.util.* -import javax.swing.* -import javax.swing.table.AbstractTableModel -import javax.swing.table.DefaultTableCellRenderer - -class ActionTable( - val actionSettings: MutableList -) : JPanel(BorderLayout()) { - - fun read(registry: ActionSettingsRegistry) { -// registry.actionSettings.clear() - rowData.map { row -> - val copy = (actionSettings.find { it.id == row[2] })!!.copy( - enabled = ((row[0] as String) == "true"), - displayText = row[1] as String - ) -// registry.actionSettings.put(copy.id, copy) - } - } - - fun write(registry: ActionSettingsRegistry) { - registry.actionSettings.values.forEach { actionSetting -> - val row = rowData.find { it[2] == actionSetting.id } - row?.let { - actionSetting.enabled = (it[0] as String) == "true" - actionSetting.displayText = it[1] as String - } - } - } - - - private val buttonPanel = JPanel() - val columnNames = arrayOf("Enabled", "Display Text", "ID") - - val rowData = actionSettings.map { - listOf(it.enabled.toString(), it.displayText, it.id).toMutableList() - }.toMutableList() - - val dataModel = object : AbstractTableModel() { - override fun getColumnName(column: Int): String { - return columnNames.get(column).toString() - } - - override fun getRowCount(): Int { - return rowData.size - } - - override fun getColumnCount(): Int { - return columnNames.size - } - - override fun getValueAt(row: Int, col: Int): Any { - return rowData[row][col]!! - } - - override fun isCellEditable(row: Int, column: Int): Boolean { - return true - } - - override fun setValueAt(value: Any, row: Int, col: Int) { - rowData[row][col] = value.toString() - fireTableCellUpdated(row, col) - } - - } - - val jtable = JBTable(dataModel) - - private val scrollpane = JBScrollPane(jtable) - - private val cloneButton = JButton(object : AbstractAction("Clone") { - override fun actionPerformed(e: ActionEvent?) { - - if (jtable.selectedRows.size != 1) { - JOptionPane.showMessageDialog(null, "Please select a single row to clone") - return - } - - val selectedRowIndex = jtable.selectedRow - val selectedSettings = actionSettings.find { - it.id == dataModel.getValueAt(selectedRowIndex, 2) - } - - val panel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP)) - val classnameField = JTextField(100) - classnameField.text = dataModel.getValueAt(selectedRowIndex, 2).toString() - panel.add(with(JPanel(HorizontalLayout(2, 0))) { - add(JLabel("New class name:")) - add(classnameField) - this - }) - val displayField = JTextField(100) - displayField.text = dataModel.getValueAt(selectedRowIndex, 1).toString() - panel.add(with(JPanel(HorizontalLayout(2,0))) { - add(JLabel("New description:")) - add(displayField) - this - }) - val options = arrayOf("OK", "Cancel") - if (JOptionPane.showOptionDialog( - null, - panel, - "Create Action", - JOptionPane.NO_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - options, - options[1] - ) == JOptionPane.OK_OPTION - ) { - if ((0 until dataModel.rowCount).toList().any { dataModel.getValueAt(it, 2) == classnameField.text }) { - JOptionPane.showMessageDialog(null, "Class name already exists") - } else { - val newRow = mutableListOf() - newRow.add("true") - newRow.add(displayField.text) - newRow.add(classnameField.text) - val newSettings = selectedSettings!!.copy( - id = classnameField.text, - displayText = displayField.text, - enabled = true, - isDynamic = true - ) - newSettings.file.writeText( - selectedSettings.file.readText().replace( - ("""(? { - projectManager.openProjects.firstOrNull() ?: projectManager.defaultProject - } - - if (it.exists()) { - ApplicationManager.getApplication().runReadAction { - val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - val fileEditorManager = FileEditorManager.getInstance(project!!) - val editor = fileEditorManager.openFile(virtualFile!!, true).firstOrNull() - } - } else { - log.warn("File not found: ${it.absolutePath}") - } - } - } - })) - - private val removeButton = JButton(object : AbstractAction("Remove") { - override fun actionPerformed(e: ActionEvent?) { - if (jtable.selectedRows.size != 1) { - JOptionPane.showMessageDialog(null, "Please select a single row to clone") - return - } - val selectedRow = jtable.selectedRow - val selectedSettings = actionSettings.find { - it.id == dataModel.getValueAt(selectedRow, 2) - } - if (selectedSettings?.isDynamic != true) { - JOptionPane.showMessageDialog(null, "Cannot remove non-dynamic action") - return - } - rowData.removeIf { - it[2] == selectedSettings.id - } - this@ActionTable.parent.invalidate() - } - }) - - init { - jtable.columnModel.getColumn(0).cellRenderer = BooleanTableCellRenderer() - jtable.columnModel.getColumn(1).cellRenderer = DefaultTableCellRenderer() - jtable.columnModel.getColumn(2).cellRenderer = DefaultTableCellRenderer() - - jtable.columnModel.getColumn(0).cellEditor = BooleanTableCellEditor() - jtable.columnModel.getColumn(1).cellEditor = JXTable.GenericEditor() - jtable.columnModel.getColumn(2).cellEditor = object : JXTable.GenericEditor() { - override fun isCellEditable(anEvent: EventObject?) = false - } - - jtable.columnModel.getColumn(0).headerRenderer = DefaultTableCellRenderer() - jtable.columnModel.getColumn(1).headerRenderer = DefaultTableCellRenderer() - jtable.columnModel.getColumn(2).headerRenderer = DefaultTableCellRenderer() - - // Set the preferred width for the first column (checkboxes) to the header label width - val headerRenderer = jtable.tableHeader.defaultRenderer - val headerValue = jtable.columnModel.getColumn(0).headerValue - val headerComp = headerRenderer.getTableCellRendererComponent(jtable, headerValue, false, false, 0, 0) - jtable.columnModel.getColumn(0).preferredWidth = headerComp.preferredSize.width - - // Set the minimum width for the second column (display text) to accommodate 100 characters - val metrics = jtable.getFontMetrics(jtable.font) - val minWidth = metrics.charWidth('m') * 32 - jtable.columnModel.getColumn(1).minWidth = minWidth - - jtable.tableHeader.defaultRenderer = DefaultTableCellRenderer() - - add(scrollpane, BorderLayout.CENTER) - buttonPanel.add(cloneButton) - buttonPanel.add(editButton) - buttonPanel.add(removeButton) - add(buttonPanel, BorderLayout.SOUTH) - } - companion object { - private val log = LoggerFactory.getLogger(ActionTable::class.java) - } -} 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 2d7f01f5..5cd07477 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -132,14 +132,6 @@ class AppSettingsComponent : com.intellij.openapi.Disposable { } } - @Name("File Actions") - var fileActions = ActionTable(AppSettingsState.instance.fileActions.actionSettings.values.map { it.copy() } - .toTypedArray().toMutableList()) - - @Name("Editor Actions") - var editorActions = ActionTable(AppSettingsState.instance.editorActions.actionSettings.values.map { it.copy() } - .toTypedArray().toMutableList()) - @Name("Editor Actions") var usage = UsageTable(ApplicationServices.usageManager) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt index c13fe582..6cea19db 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt @@ -5,14 +5,10 @@ import com.github.simiacryptus.aicoder.util.UITools open class AppSettingsConfigurable : UIAdapter(AppSettingsState.instance) { override fun read(component: AppSettingsComponent, settings: AppSettingsState) { UITools.readKotlinUIViaReflection(component, settings) - component.editorActions.read(settings.editorActions) - component.fileActions.read(settings.fileActions) } override fun write(settings: AppSettingsState, component: AppSettingsComponent) { UITools.writeKotlinUIViaReflection(settings, component) - component.editorActions.write(settings.editorActions) - component.fileActions.write(settings.fileActions) } override fun getPreferredFocusedComponent() = component?.temperature 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 bc335d59..c67ca23d 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.util.xmlb.XmlSerializerUtil +import com.jetbrains.rd.util.firstOrNull import com.simiacryptus.jopenai.models.ChatModels import com.simiacryptus.jopenai.util.JsonUtil import java.io.File @@ -40,10 +41,6 @@ data class AppSettingsState( ) : PersistentStateComponent { private var onSettingsLoadedListeners = mutableListOf<() -> Unit>() - val editorActions: ActionSettingsRegistry - get() = ActionSettingsRegistry(pluginHome.resolve("editorActions").apply { mkdirs() }) - val fileActions: ActionSettingsRegistry - get() = ActionSettingsRegistry(pluginHome.resolve("fileActions").apply { mkdirs() }) private val recentCommands = mutableMapOf() fun defaultSmartModel() = smartModel.chatModel() @@ -104,8 +101,6 @@ data class AppSettingsState( if (devActions != other.devActions) return false if (editRequests != other.editRequests) return false if (pluginHome != other.pluginHome) return false - if (editorActions != other.editorActions) return false - if (fileActions != other.fileActions) return false if (recentCommands != other.recentCommands) return false return true @@ -127,8 +122,6 @@ data class AppSettingsState( result = 31 * result + devActions.hashCode() result = 31 * result + editRequests.hashCode() result = 31 * result + pluginHome.hashCode() - result = 31 * result + editorActions.hashCode() - result = 31 * result + fileActions.hashCode() result = 31 * result + recentCommands.hashCode() return result } @@ -145,7 +138,7 @@ data class AppSettingsState( fun String.chatModel(): ChatModels { return ChatModels.values().entries.firstOrNull { it.value.modelName == this || it.key == this - }?.value ?: throw IllegalArgumentException("Unknown model: ${this}") + }?.value ?: ChatModels.GPT35Turbo } } } 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 7fa358b2..7c3a034b 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/StaticAppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/StaticAppSettingsConfigurable.kt @@ -103,14 +103,6 @@ class StaticAppSettingsConfigurable : AppSettingsConfigurable() { add(component.usage, BorderLayout.CENTER) }) - tabbedPane.addTab("File Actions", JPanel(BorderLayout()).apply { - add(component.fileActions, BorderLayout.CENTER) - }) - - tabbedPane.addTab("Editor Actions", JPanel(BorderLayout()).apply { - add(component.editorActions, BorderLayout.CENTER) - }) - return tabbedPane } @@ -134,8 +126,6 @@ class StaticAppSettingsConfigurable : AppSettingsConfigurable() { val key = value.name model.addRow(arrayOf(key, settings.apiKey?.get(key) ?: "", settings.apiBase?.get(key) ?: value.base)) } - component.editorActions.read(settings.editorActions) - component.fileActions.read(settings.fileActions) } catch (e: Exception) { log.warn("Error setting UI", e) } @@ -174,8 +164,6 @@ class StaticAppSettingsConfigurable : AppSettingsConfigurable() { } } } - component.editorActions.write(settings.editorActions) - component.fileActions.write(settings.fileActions) } catch (e: Exception) { log.warn("Error reading UI", e) } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/EditorMenu.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/EditorMenu.kt index 87363f0f..3195d28f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/EditorMenu.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/EditorMenu.kt @@ -1,12 +1,13 @@ package com.github.simiacryptus.aicoder.ui -import com.github.simiacryptus.aicoder.config.AppSettingsState import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent open class EditorMenu : com.intellij.openapi.actionSystem.DefaultActionGroup() { override fun getChildren(e: AnActionEvent?): Array { - return AppSettingsState.instance.editorActions.edit(super.getChildren(e)) + val children = super.getChildren(e) + return children +// return AppSettingsState.instance.editorActions.edit(children) } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt index b0d0b037..479be8ff 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt @@ -1,11 +1,12 @@ package com.github.simiacryptus.aicoder.ui -import com.github.simiacryptus.aicoder.config.AppSettingsState import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent open class ProjectMenu : com.intellij.openapi.actionSystem.DefaultActionGroup() { override fun getChildren(e: AnActionEvent?): Array { - return AppSettingsState.instance.fileActions.edit(super.getChildren(e)) + val children = super.getChildren(e) + return children +// return AppSettingsState.instance.fileActions.edit(children) } } \ No newline at end of file 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 bb991e4e..140b4cb3 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt @@ -63,21 +63,25 @@ class IdeaOpenAIClient : OpenAIClient( lastEvent ?: return super.chat(chatRequest, model) if (isInRequest.getAndSet(true)) { val response = super.chat(chatRequest, model) - UITools.logAction( - """ + if(null != response.usage) { + UITools.logAction( + """ |Chat Response: ${JsonUtil.toJson(response.usage!!)} """.trimMargin().trim() - ) + ) + } return response } else { try { if (!AppSettingsState.instance.editRequests) { val response = super.chat(chatRequest, model) - UITools.logAction( - """ + if(null != response.usage) { + UITools.logAction( + """ |Chat Response: ${JsonUtil.toJson(response.usage!!)} """.trimMargin().trim() - ) + ) + } return response } return withJsonDialog(chatRequest, { chatRequest -> @@ -85,11 +89,13 @@ class IdeaOpenAIClient : OpenAIClient( lastEvent!!.project, "OpenAI Request", true, suppressProgress = false ) { val response = super.chat(chatRequest, model) - UITools.logAction( - """ + if(null != response.usage) { + UITools.logAction( + """ |Chat Response: ${JsonUtil.toJson(response.usage!!)} """.trimMargin().trim() - ) + ) + } response } }, "Edit Chat Request") diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt index 0951021d..90f38d25 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt @@ -1,18 +1,16 @@ package com.github.simiacryptus.aicoder.util -import com.github.simiacryptus.aicoder.config.ActionSettingsRegistry +//import com.github.simiacryptus.aicoder.config.ActionSettingsRegistry import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.Name import com.google.common.util.concurrent.* import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Document import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task @@ -21,7 +19,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBPasswordField @@ -825,8 +822,8 @@ object UITools { log.info("showOptionDialog = $showOptionDialog") } else if (e.matches { ScriptException::class.java.isAssignableFrom(it.javaClass) }) { val scriptException = e.get { ScriptException::class.java.isAssignableFrom(it.javaClass) } as ScriptException? - val dynamicActionException = - e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? +// val dynamicActionException = +// e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? val formBuilder = FormBuilder.createFormBuilder() formBuilder.addLabeledComponent( @@ -838,8 +835,6 @@ object UITools { bugReportTextArea.columns = 80 bugReportTextArea.isEditable = false bugReportTextArea.text = """ - |Action Name: ${dynamicActionException?.actionSetting?.displayText} - |Action ID: ${dynamicActionException?.actionSetting?.id} |Script Error: ${scriptException?.message} | |Error Details: @@ -849,39 +844,6 @@ object UITools { |""".trimMargin() formBuilder.addLabeledComponent("Error Report", wrapScrollPane(bugReportTextArea)) - if (dynamicActionException?.actionSetting?.isDynamic == false) { - val openButton = JXButton("Revert to Default") - openButton.addActionListener { - dynamicActionException?.actionSetting?.file?.delete() - } - formBuilder.addLabeledComponent("Revert Built-in Action", openButton) - } - - if (null != dynamicActionException) { - val openButton = JXButton("Open Dynamic Action") - openButton.addActionListener { - dynamicActionException?.file?.let { - val project = ApplicationManager.getApplication().runReadAction { - com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() - } - if (it.exists()) { - ApplicationManager.getApplication().invokeLater { - val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) - } - } else { - Thread { - showOptionDialog( - formBuilder.panel, "Dismiss", title = "Error - File Not Found", modal = true - ) - }.start() - } - } - - } - formBuilder.addLabeledComponent("View Code", openButton) - } - val supressFutureErrors = JCheckBox("Suppress Future Error Popups") supressFutureErrors.isSelected = false formBuilder.addComponent(supressFutureErrors) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 418dd7df..7460d6b0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -341,13 +341,15 @@ --> - - - - - + + + + diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt index b0ef0ad3..dbced010 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/util/ApxPatchUtilTest.kt @@ -1,5 +1,6 @@ package com.github.simiacryptus.aicoder.util +import com.github.simiacryptus.diff.ApxPatchUtil import org.junit.Assert.assertTrue import org.junit.Test diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/util/DiffMatchPatchTest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/util/DiffMatchPatchTest.kt index 7e36f40d..2ae05b30 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/util/DiffMatchPatchTest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/util/DiffMatchPatchTest.kt @@ -1,8 +1,8 @@ package com.github.simiacryptus.aicoder.util -import com.github.simiacryptus.aicoder.util.DiffMatchPatch -import com.github.simiacryptus.aicoder.util.DiffMatchPatch.Diff -import com.github.simiacryptus.aicoder.util.DiffMatchPatch.Operation.* +import com.github.simiacryptus.diff.DiffMatchPatch +import com.github.simiacryptus.diff.DiffMatchPatch.Diff +import com.github.simiacryptus.diff.DiffMatchPatch.Operation.* import org.junit.Assert import org.junit.Test import java.util.*