diff --git a/build.gradle.kts b/build.gradle.kts
index 3ec16c6f..f5abc093 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -26,7 +26,7 @@ repositories {
val kotlin_version = "1.9.21"
val jetty_version = "11.0.18"
val slf4j_version = "2.0.9"
-val skyenet_version = "1.0.47"
+val skyenet_version = "1.0.48"
val remoterobot_version = "0.11.21"
dependencies {
diff --git a/gradle.properties b/gradle.properties
index cb291b81..61ff304b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
pluginName=intellij-aicoder
pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder
-pluginVersion=1.2.27
+pluginVersion=1.2.28
jvmArgs=-Xmx8g
org.gradle.jvmargs=-Xmx8g
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 79ba581a..a7eea799 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.actions.dev.AppServer
import com.github.simiacryptus.aicoder.config.AppSettingsState
import com.github.simiacryptus.aicoder.util.CodeChatSocketManager
import com.github.simiacryptus.aicoder.util.ComputerLanguage
-import com.github.simiacryptus.aicoder.util.SimpleDiffUtil
+import com.github.simiacryptus.aicoder.util.addApplyDiffLinks
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.command.WriteCommandAction
@@ -29,7 +29,6 @@ class DiffChatAction : BaseAction() {
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
@@ -66,32 +65,13 @@ class DiffChatAction : BaseAction() {
get() = super.systemPrompt + """
Provide code patches in diff format within ```diff code blocks.
The diff format should use + for line additions, - for line deletions.
- The diff should include 2 lines of context before and after every change.
+ The diff should include sufficient context before every change to identify the location.
""".trimIndent()
-
- val diffPattern = """(?s)(?()
override fun renderResponse(response: String): String {
- val matches = diffPattern.findAll(response).distinct()
- val withLinks = matches.fold(response) { markdown, diffBlock ->
- val diffVal = diffBlock.value
- val hrefLink = hrefLink("Apply Diff") {
- try {
- if(fullPatch.contains(diffVal)) return@hrefLink
- fullPatch.add(diffVal)
- val newCode = fullPatch.fold(rawText) {
- lines, patch -> SimpleDiffUtil.patch(lines, patch)
- }
- WriteCommandAction.runWriteCommandAction(e.project) {
- document.replaceString(selectionStart, selectionEnd, newCode)
- }
- send(divInitializer(cancelable = false) + """
Diff Applied
""")
- } catch (e: Throwable) {
- log.warn("Error applying diff", e)
- send(divInitializer(cancelable = false) + """${e.message}
""")
- }
+ val withLinks = addApplyDiffLinks(rawText, response) { newCode ->
+ WriteCommandAction.runWriteCommandAction(e.project) {
+ document.replaceString(selectionStart, selectionEnd, newCode)
}
- markdown.replace(diffVal, diffVal + "\n" + hrefLink)
}
val html = renderMarkdown(withLinks)
return """$html
"""
@@ -130,4 +110,4 @@ class DiffChatAction : BaseAction() {
}
}
-}
+}
\ 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
new file mode 100644
index 00000000..170a5e16
--- /dev/null
+++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt
@@ -0,0 +1,121 @@
+package com.github.simiacryptus.aicoder.actions.generic
+
+import com.github.simiacryptus.aicoder.ApplicationEvents
+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.ComputerLanguage
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.CommonDataKeys
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.simiacryptus.skyenet.core.platform.ApplicationServices
+import com.simiacryptus.skyenet.core.platform.Session
+import com.simiacryptus.skyenet.core.platform.StorageInterface
+import com.simiacryptus.skyenet.core.platform.User
+import com.simiacryptus.skyenet.webui.application.ApplicationServer
+import com.simiacryptus.skyenet.webui.chat.ChatServer
+import com.simiacryptus.skyenet.webui.chat.ChatSocketManager
+import com.simiacryptus.skyenet.webui.session.SocketManager
+import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown
+import org.slf4j.LoggerFactory
+import java.awt.Desktop
+import java.io.File
+
+class LineFilterChatAction : BaseAction() {
+
+ 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.defaultChatModel(),
+ userInterfacePrompt = """
+ |# `$filename`
+ |
+ |```$language
+ |$code
+ |```
+ """.trimMargin().trim(),
+ systemPrompt = """
+ |You are a helpful AI that helps people with coding.
+ |
+ |You will be answering questions about the following code located in `$filename`:
+ |
+ |```$language
+ |$codelines
+ |```
+ |
+ |Responses may use markdown formatting. Lines from the prompt can be included by using the line number in a response line (e.g. `\nLINE\n`).
+ |
+ |For example:
+ |
+ |```text
+ |001
+ |## Injected subtitle
+ |
+ |025
+ |026
+ |
+ |013
+ |014
+ |```
+ """.trimMargin(),
+ api = api,
+ applicationClass = ApplicationServer::class.java,
+ storage = ApplicationServices.dataStorageFactory(root),
+ ) {
+ override fun canWrite(user: User?): Boolean = true
+ override fun renderResponse(response: String): 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)
+
+ 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
+
+ companion object {
+ private val log = LoggerFactory.getLogger(LineFilterChatAction::class.java)
+ private val agents = mutableMapOf()
+ val root: File get() = File(ApplicationEvents.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
+ }
+
+ }
+}
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
new file mode 100644
index 00000000..5b323d95
--- /dev/null
+++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt
@@ -0,0 +1,508 @@
+package com.github.simiacryptus.aicoder.actions.generic
+
+import com.github.simiacryptus.aicoder.ApplicationEvents
+import com.github.simiacryptus.aicoder.actions.BaseAction
+import com.github.simiacryptus.aicoder.actions.dev.AppServer
+import com.github.simiacryptus.aicoder.util.UITools
+import com.github.simiacryptus.aicoder.util.addApplyDiffLinks
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.vfs.VirtualFile
+import com.simiacryptus.jopenai.API
+import com.simiacryptus.jopenai.ApiModel
+import com.simiacryptus.jopenai.describe.Description
+import com.simiacryptus.jopenai.models.ChatModels
+import com.simiacryptus.jopenai.proxy.ValidatedObject
+import com.simiacryptus.jopenai.util.ClientUtil.toContentList
+import com.simiacryptus.jopenai.util.JsonUtil
+import com.simiacryptus.skyenet.AgentPatterns
+import com.simiacryptus.skyenet.core.actors.ActorSystem
+import com.simiacryptus.skyenet.core.actors.BaseActor
+import com.simiacryptus.skyenet.core.actors.ParsedActor
+import com.simiacryptus.skyenet.core.actors.SimpleActor
+import com.simiacryptus.skyenet.core.platform.*
+import com.simiacryptus.skyenet.core.platform.file.DataStorage
+import com.simiacryptus.skyenet.webui.application.ApplicationInterface
+import com.simiacryptus.skyenet.webui.application.ApplicationServer
+import com.simiacryptus.skyenet.webui.chat.ChatServer
+import com.simiacryptus.skyenet.webui.servlet.ToolServlet
+import com.simiacryptus.skyenet.webui.session.SessionTask
+import com.simiacryptus.skyenet.webui.util.MarkdownUtil
+import org.slf4j.LoggerFactory
+import java.awt.Desktop
+import java.io.File
+import java.util.concurrent.atomic.AtomicBoolean
+
+private val VirtualFile.toFile: File get() = File(this.path)
+
+class WebDevAction : BaseAction() {
+
+ 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
+ }
+ 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)
+ }
+
+ 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
+ ) {
+ 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,
+ )
+
+ override val settingsClass: Class<*> get() = Settings::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun initSettings(session: Session): T? = Settings() as T
+ }
+
+ class WebDevAgent(
+ val api: API,
+ dataStorage: StorageInterface,
+ session: Session,
+ user: User?,
+ val ui: ApplicationInterface,
+ val model: ChatModels = ChatModels.GPT35Turbo,
+ val tools: List = emptyList(),
+ 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 = """
+ You will translate the user request into a javascript file for use in a rich javascript application.
+ """.trimIndent(), model = model
+ ),
+ ActorTypes.CssCodingActor to SimpleActor(
+ prompt = """
+ You will translate the user request into a CSS file for use in a rich javascript application.
+ """.trimIndent(), model = model
+ ),
+ ActorTypes.ArchitectureDiscussionActor to ParsedActor(
+ parserClass = PageResourceListParser::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.
+ Identify key HTML classes and element IDs that will be used to bind the application to the HTML.
+ 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
+ ),
+ ActorTypes.CodeReviewer to SimpleActor(
+ prompt = """
+ Analyze the code summarized in the user's header-labeled code blocks.
+ Review, look for bugs, and provide fixes.
+ Provide implementations for missing functions.
+
+ Response should use one or more code patches in diff format within ```diff code blocks.
+ Each diff should be preceded by a header that identifies the file being modified.
+ The diff format should use + for line additions, - for line deletions.
+ The diff should include 2 lines of context before and after every change.
+
+ Example:
+
+ Explanation text
+
+ ### scripts/filename.js
+ ```diff
+ - const b = 2;
+ + const a = 1;
+ ```
+
+ Continued text
+ """.trimIndent(),
+ model = model,
+ ),
+ ActorTypes.EtcCodingActor to SimpleActor(
+ prompt = """
+ You will translate the user request into a file for use in a web application.
+ """.trimIndent(),
+ model = model
+ ),
+ ),
+ ) : ActorSystem(actorMap, dataStorage, user, session) {
+ enum class ActorTypes {
+ HtmlCodingActor,
+ JavascriptCodingActor,
+ CssCodingActor,
+ ArchitectureDiscussionActor,
+ CodeReviewer,
+ EtcCodingActor,
+ }
+
+ val architectureDiscussionActor by lazy { getActor(ActorTypes.ArchitectureDiscussionActor) as ParsedActor }
+ val htmlActor by lazy { getActor(ActorTypes.HtmlCodingActor) as SimpleActor }
+ val javascriptActor by lazy { getActor(ActorTypes.JavascriptCodingActor) as SimpleActor }
+ val cssActor by lazy { getActor(ActorTypes.CssCodingActor) as SimpleActor }
+ val codeReviewer by lazy { getActor(ActorTypes.CodeReviewer) as SimpleActor }
+ val codeFiles = mutableMapOf()
+ val etcActor by lazy { getActor(ActorTypes.EtcCodingActor) as SimpleActor }
+
+ fun start(
+ userMessage: String,
+ ) {
+ val architectureResponse = AgentPatterns.iterate(
+ input = userMessage,
+ heading = userMessage,
+ actor = architectureDiscussionActor,
+ toInput = { listOf(it) },
+ api = api,
+ ui = ui,
+ outputFn = { task, design ->
+ task.add(MarkdownUtil.renderMarkdown("${design.text}\n\n```json\n${JsonUtil.toJson(design.obj)}\n```"))
+ }
+ )
+
+ val task = ui.newTask()
+ 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(MarkdownUtil.renderMarkdown("```json\n${JsonUtil.toJson(architectureResponse.obj)}\n```"))
+ 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
+ )
+
+ 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()
+ }\n$code\n```"
+ }
+
+
+ fun outputFn(task: SessionTask, design: String): StringBuilder? {
+ //val task = ui.newTask()
+ return task.complete(
+ MarkdownUtil.renderMarkdown(
+ ui.socketManager.addApplyDiffLinks(
+ codeFiles,
+ design
+ ) { newCodeMap ->
+ newCodeMap.forEach { (path, newCode) ->
+ val prev = codeFiles[path]
+ if (prev != newCode) {
+ codeFiles[path] = newCode
+ task.complete(
+ "$path Updated"
+ )
+ }
+ }
+ })
+ )
+ }
+ try {
+ var task = ui.newTask()
+ task.add(message = MarkdownUtil.renderMarkdown(codeSummary()))
+ 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(MarkdownUtil.renderMarkdown(userResponse))
+ val codeSummary = codeSummary()
+ task.add(MarkdownUtil.renderMarkdown(codeSummary))
+ 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)
+ }
+ }
+
+ 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(MarkdownUtil.renderMarkdown("```${languages.first()}\n$code\n```"))
+ 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.textInput { feedback ->
+ responseAction(task, "Revising...", formHandle!!, formText) {
+ //val task = ui.newTask()
+ try {
+ task.echo(MarkdownUtil.renderMarkdown(feedback))
+ 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"
+ )
+ 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()
+ })
+ }
+ }
+ }
+
+ companion object {
+ private val log = LoggerFactory.getLogger(WebDevAction::class.java)
+ private val agents = mutableMapOf()
+ val root: File get() = File(ApplicationEvents.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
+ }
+
+ interface PageResourceListParser : java.util.function.Function {
+ @Description("Parse out a list of files and descriptions in this project")
+ override fun apply(html: String): PageResourceList
+ }
+
+ 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
+ }
+ }
+
+ }
+
+}
diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt
index e7ae8ae7..a7d9eb42 100644
--- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt
+++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt
@@ -11,7 +11,7 @@ import java.util.stream.Collectors
class ActionSettingsRegistry {
val actionSettings: MutableMap = HashMap()
- private val version = 2.0042
+ private val version = 2.0049
fun edit(superChildren: Array): Array {
val children = superChildren.toList().toMutableList()
diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt
deleted file mode 100644
index 1da27a99..00000000
--- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-package com.github.simiacryptus.aicoder.util
-
-import org.apache.commons.text.similarity.LevenshteinDistance
-
-object SimpleDiffUtil {
-
-
- fun patch(source: String, patch: String): String {
- val sourceLines = source.lines()
- val patchLines = patch.lines()
-
- // This will hold the final result
- val result = mutableListOf()
-
- // This will keep track of the current line in the source file
- var sourceIndex = 0
-
- // Process each line in the patch
- for (patchLine in patchLines.map { it.trim() }) {
- when {
- // If the line starts with "---" or "+++", it's a file indicator line, skip it
- patchLine.startsWith("---") || patchLine.startsWith("+++") -> continue
-
- // If the line starts with "@@", it's a hunk header
- patchLine.startsWith("@@") -> continue
-
- // If the line starts with "-", it's a deletion, skip the corresponding source line but otherwise treat it as a context line
- patchLine.startsWith("-") -> {
- sourceIndex = onDelete(patchLine, sourceIndex, sourceLines, result)
- }
-
- // If the line starts with "+", it's an addition, add it to the result
- patchLine.startsWith("+") -> {
- result.add(patchLine.substring(1))
- }
-
- // \d+\: ___ is a line number, strip it
- patchLine.matches(Regex("\\d+:.*")) -> {
- sourceIndex = onContextLine(patchLine.substringAfter(":"), sourceIndex, sourceLines, result)
- }
-
- // it's a context line, advance the source cursor
- else -> {
- sourceIndex = onContextLine(patchLine, sourceIndex, sourceLines, result)
- }
- }
- }
-
- // Append any remaining lines from the source file
- while (sourceIndex < sourceLines.size) {
- result.add(sourceLines[sourceIndex])
- sourceIndex++
- }
-
- return result.joinToString("\n")
- }
-
- private fun onDelete(
- patchLine: String,
- sourceIndex: Int,
- sourceLines: List,
- result: MutableList
- ): Int {
- var sourceIndex1 = sourceIndex
- val delLine = patchLine.substring(1)
- val sourceIndexSearch = lookAheadFor(sourceIndex1, sourceLines, delLine)
- if (sourceIndexSearch > 0 && sourceIndexSearch + 1 < sourceLines.size) {
- val contextChunk = sourceLines.subList(sourceIndex1, sourceIndexSearch)
- result.addAll(contextChunk)
- sourceIndex1 = sourceIndexSearch + 1
- } else {
- println("Deletion line not found in source file: $delLine")
- // Ignore
- }
- return sourceIndex1
- }
-
- private fun onContextLine(
- patchLine: String,
- sourceIndex: Int,
- sourceLines: List,
- result: MutableList
- ): Int {
- var sourceIndex1 = sourceIndex
- val sourceIndexSearch = lookAheadFor(sourceIndex1, sourceLines, patchLine)
- if (sourceIndexSearch > 0 && sourceIndexSearch + 1 < sourceLines.size) {
- val contextChunk = sourceLines.subList(sourceIndex1, sourceIndexSearch + 1)
- result.addAll(contextChunk)
- sourceIndex1 = sourceIndexSearch + 1
- } else {
- println("Context line not found in source file: $patchLine")
- // Ignore
- }
- return sourceIndex1
- }
-
- private fun lookAheadFor(
- sourceIndex: Int,
- sourceLines: List,
- patchLine: String
- ): Int {
- var sourceIndexSearch = sourceIndex
- while (sourceIndexSearch < sourceLines.size) {
- if (lineMatches(patchLine, sourceLines[sourceIndexSearch++])) return sourceIndexSearch - 1
- }
- return - 1
- }
-
- private fun lineMatches(
- a: String,
- b: String,
- factor: Double = 0.3
- ): Boolean {
- val threshold = (Math.max(a.trim().length, b.trim().length) * factor).toInt()
- val levenshteinDistance = LevenshteinDistance(5)
- val dist = levenshteinDistance.apply(a.trim(), b.trim())
- if (dist >= 0) {
- if (dist <= threshold) {
- return true
- } else {
- return false
- }
- } else {
- return false
- }
- }
-}
-
-
-
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index d215424d..d20708e8 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -110,6 +110,25 @@
-->
+
+
+
+
+
+
+
+