diff --git a/docs/actions_best_practices.md b/docs/actions_best_practices.md index 2cbf247b..7cc0f57d 100644 --- a/docs/actions_best_practices.md +++ b/docs/actions_best_practices.md @@ -1,177 +1,273 @@ -Here's a best practices document for actions in this project: - # AI Coder Action Best Practices -## Action Types +## 1. Choosing the Right Base Class + +Choose the appropriate base class for your action: + +### BaseAction + +Basic usage: + +```kotlin +class SimpleAction : BaseAction( + name = "My Action", + description = "Does something simple" +) { + override fun handle(event: AnActionEvent) { + val project = event.project ?: return + UITools.run(project, "Processing", true) { progress -> + // Action logic here with progress indication + progress.text = "Doing work..." + } + } +} +``` + +### SelectionAction -The project has several base action types that should be used appropriately: +* Provides automatic selection handling and language detection +* Example implementations: ApplyPatchAction, CodeFormatterAction + Advanced usage: -1. `BaseAction` - Base class for all actions -2. `SelectionAction` - For actions that operate on selected text -3. `FileContextAction` - For actions that operate on files/folders +```kotlin +class CodeFormatterAction : SelectionAction() { + // Custom configuration class + data class FormatterConfig( + val style: String, + val indent: Int + ) + + override fun getConfig(project: Project?): FormatterConfig? { + return FormatterConfig( + style = "google", + indent = 4 + ) + } -Choose the appropriate base class based on your action's needs. + override fun processSelection(state: SelectionState, config: FormatterConfig?): String { + // Access selection context + val code = state.selectedText ?: return "" + val lang = state.language ?: return code + val indent = state.indent?.toString() ?: " " + // Process with configuration + return formatCode(code, lang, config?.style, config?.indent ?: 4) + } +} +``` + +### FileContextAction + +* Provides progress indication and cancellation support +* Manages file refresh and editor updates + Complete example: + +```kotlin +class FileProcessorAction : FileContextAction() { + data class ProcessorConfig( + val outputDir: String, + val options: Map + ) + + override fun getConfig(project: Project?, e: AnActionEvent): ProcessorConfig? { + val dir = UITools.chooseDirectory(project, "Select Output Directory") + return dir?.let { ProcessorConfig(it.path, mapOf()) } + } + + override fun processSelection(state: SelectionState, config: ProcessorConfig?, progress: ProgressIndicator): Array { + progress.text = "Processing ${state.selectedFile.name}" + val outputFile = File(config?.outputDir, "processed_${state.selectedFile.name}") + outputFile.parentFile.mkdirs() + // Process file with progress updates + progress.isIndeterminate = false + processFileWithProgress(state.selectedFile, outputFile, progress) + return arrayOf(outputFile) + } +} +``` ## Core Principles ### 1. Thread Safety -- Always run long operations in background threads -- Use `WriteCommandAction` for document modifications -- Protect shared resources with appropriate synchronization +Best practices for thread management: +* Use WriteCommandAction for document modifications +* Avoid blocking EDT (Event Dispatch Thread) +* Handle background tasks properly ```kotlin -Thread { - try { - UITools.redoableTask(e) { - // Long running operation +// Good example * proper thread handling: +WriteCommandAction.runWriteCommandAction(project) { + try { + UITools.run(project, "Processing", true) { progress -> + progress.isIndeterminate = false + progress.text = "Processing..." + ApplicationManager.getApplication().executeOnPooledThread { + // Long running operation + progress.fraction = 0.5 + } + } + } catch (e: Throwable) { + UITools.error(log, "Error", e) } - } catch (e: Throwable) { - UITools.error(log, "Error", e) - } -}.start() +} + +// Bad example * avoid: +Thread { + document.setText("new text") // Don't modify documents outside WriteCommandAction +}.start() ``` ### 2. Error Handling -- Wrap operations in try-catch blocks -- Log errors appropriately using the logger -- Show user-friendly error messages -- Make actions fail gracefully +Comprehensive error handling strategy: +* Use structured error handling +* Provide user feedback +* Log errors appropriately ```kotlin +// Good example: try { - // Action logic -} catch (e: Exception) { - log.error("Error in action", e) - UITools.showErrorDialog(project, e.message, "Error") + processFile(file) +} catch (e: IOException) { + log.error("Failed to process file: ${file.path}", e) + val choice = UITools.showErrorDialog( + project, + "Failed to process file: ${e.message}\nWould you like to retry?", + "Error", + arrayOf("Retry", "Cancel") + ) + if (choice == 0) { + // Retry logic + processFile(file) + } +} finally { + cleanup() } ``` - ### 3. Progress Indication -- Use progress indicators for long operations -- Show meaningful progress messages -- Allow cancellation where appropriate +Guidelines for progress feedback: +* Update progress frequently +* Include operation details in progress text ```kotlin -UITools.run(project, "Operation Name", true) { progress -> - progress.text = "Step description..." - progress.fraction = 0.5 - // Operation logic +UITools.run(project, "Processing Files", true) { progress -> + progress.text = "Initializing..." + files.forEachIndexed { index, file -> + if (progress.isCanceled) throw InterruptedException() + progress.fraction = index.toDouble() / files.size + progress.text = "Processing ${file.name} (${index + 1}/${files.size})" + processFile(file) + } } ``` ### 4. Configuration -- Use `getConfig()` to get user input/settings -- Validate configurations before proceeding -- Store recent configurations where appropriate -- Use `AppSettingsState` for persistent settings - +* Use `getConfig()` to get user input/settings +* Validate configurations before proceeding +* Store recent configurations where appropriate +* Use `AppSettingsState` for persistent settings ```kotlin override fun getConfig(project: Project?, e: AnActionEvent): Settings { - return Settings( - UITools.showDialog( - project, - SettingsUI::class.java, - UserSettings::class.java, - "Dialog Title" - ), - project - ) + return Settings( + UITools.showDialog( + project, + SettingsUI::class.java, + UserSettings::class.java, + "Dialog Title" + ), + project + ) } ``` - ### 5. Action Enablement -- Implement `isEnabled()` to control when action is available -- Check for null safety -- Verify required context (files, selection, etc) -- Consider language support requirements - +* Implement `isEnabled()` to control when action is available +* Check for null safety +* Verify required context (files, selection, etc) +* Consider language support requirements ```kotlin override fun isEnabled(event: AnActionEvent): Boolean { - if (!super.isEnabled(event)) return false - val file = UITools.getSelectedFile(event) ?: return false - return isLanguageSupported(getComputerLanguage(event)) + if (!super.isEnabled(event)) return false + val file = UITools.getSelectedFile(event) ?: return false + return isLanguageSupported(getComputerLanguage(event)) } ``` ### 6. UI Integration -- Use IntelliJ UI components and dialogs -- Follow IntelliJ UI guidelines -- Provide appropriate feedback to users -- Support undo/redo operations +* Use IntelliJ UI components and dialogs +* Follow IntelliJ UI guidelines +* Provide appropriate feedback to users +* Support undo/redo operations ### 7. AI Integration -- Use appropriate models for the task -- Handle API errors gracefully -- Provide meaningful prompts -- Process AI responses appropriately - +* Use appropriate models for the task +* Handle API errors gracefully +* Provide meaningful prompts +* Process AI responses appropriately ```kotlin val response = ChatProxy( - clazz = API::class.java, - api = api, - model = AppSettingsState.instance.smartModel.chatModel(), - temperature = AppSettingsState.instance.temperature + clazz = API::class.java, + api = api, + model = AppSettingsState.instance.smartModel.chatModel(), + temperature = AppSettingsState.instance.temperature ).create() ``` ### 8. Code Organization -- Keep actions focused on a single responsibility -- Extract common functionality to utility classes -- Use appropriate inheritance hierarchy -- Follow Kotlin coding conventions +* Keep actions focused on a single responsibility +* Extract common functionality to utility classes +* Use appropriate inheritance hierarchy +* Follow Kotlin coding conventions ### 9. Documentation -- Document action purpose and usage -- Include example usage where helpful -- Document configuration options -- Explain any special requirements +* Document action purpose and usage +* Include example usage where helpful +* Document configuration options +* Explain any special requirements ### 10. Testing -- Test action enablement logic -- Test configuration validation -- Test error handling -- Test with different file types/languages +* Test action enablement logic +* Test configuration validation +* Test error handling +* Test with different file types/languages ## Common Patterns ### Chat Actions -- Use `SessionProxyServer` for chat interfaces -- Configure appropriate chat models -- Handle chat session lifecycle -- Support conversation context +* Use `SessionProxyServer` for chat interfaces +* Configure appropriate chat models +* Handle chat session lifecycle +* Support conversation context ### File Operations -- Use `FileContextAction` base class -- Handle file system operations safely -- Support undo/redo -- Refresh file system after changes +* Use `FileContextAction` base class +* Handle file system operations safely +* Support undo/redo +* Refresh file system after changes ### Code Modifications -- Use `SelectionAction` for code changes -- Support appropriate languages -- Handle code formatting -- Preserve indentation +* Use `SelectionAction` for code changes +* Support appropriate languages +* Handle code formatting +* Preserve indentation ## Specific Guidelines @@ -198,13 +294,13 @@ val response = ChatProxy( ## Best Practices Checklist -- [ ] Extends appropriate base class -- [ ] Implements proper error handling -- [ ] Shows progress for long operations -- [ ] Validates configurations -- [ ] Implements proper enablement logic -- [ ] Follows UI guidelines -- [ ] Handles AI integration properly -- [ ] Includes proper documentation -- [ ] Supports undo/redo -- [ ] Includes appropriate tests +* [ ] Extends appropriate base class +* [ ] Implements proper error handling +* [ ] Shows progress for long operations +* [ ] Validates configurations +* [ ] Implements proper enablement logic +* [ ] Follows UI guidelines +* [ ] Handles AI integration properly +* [ ] Includes proper documentation +* [ ] Supports undo/redo +* [ ] Includes appropriate tests \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt index 0efc61ce..fad1e9d0 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.actions +package com.github.simiacryptus.aicoder.actions import com.github.simiacryptus.aicoder.util.IdeaChatClient import com.github.simiacryptus.aicoder.util.IdeaOpenAIClient @@ -33,7 +33,7 @@ abstract class BaseAction( final override fun actionPerformed(e: AnActionEvent) { UITools.logAction( """ - |Action: ${javaClass.simpleName} + Action: ${javaClass.simpleName} """.trimMargin().trim() ) IdeaChatClient.lastEvent = e @@ -51,4 +51,4 @@ abstract class BaseAction( val log by lazy { LoggerFactory.getLogger(javaClass) } val scheduledPool = java.util.concurrent.Executors.newScheduledThreadPool(1) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt index 156bd5d1..4064dd00 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt @@ -4,12 +4,22 @@ import com.github.simiacryptus.aicoder.actions.SelectionAction import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy +import java.awt.Component import javax.swing.JOptionPane +/** + * Action that allows custom editing of code selections using AI. + * Supports multiple languages and provides custom edit instructions. + */ + open class CustomEditAction : SelectionAction() { + private val log = Logger.getInstance(CustomEditAction::class.java) override fun getActionUpdateThread() = ActionUpdateThread.BGT interface VirtualAPI { @@ -53,23 +63,49 @@ open class CustomEditAction : SelectionAction() { return chatProxy.create() } - override fun getConfig(project: Project?): String { + override fun getConfig(project: Project?): String? { return UITools.showInputDialog( - null, "Instruction:", "Edit Code", JOptionPane.QUESTION_MESSAGE - //, AppSettingsState.instance.getRecentCommands("customEdits").mostRecentHistory - ) as String? ?: "" + project as Component?, + "Enter edit instruction:", + "Edit Code", + JOptionPane.QUESTION_MESSAGE + ) as String? } override fun processSelection(state: SelectionState, instruction: String?): String { if (instruction == null || instruction.isBlank()) return state.selectedText ?: "" - val settings = AppSettingsState.instance - val outputHumanLanguage = AppSettingsState.instance.humanLanguage - settings.getRecentCommands("customEdits").addInstructionToHistory(instruction) - return proxy.editCode( - state.selectedText ?: "", - instruction, - state.language?.name ?: "", - outputHumanLanguage - ).code ?: state.selectedText ?: "" + return try { + UITools.run(state.project, "Processing Edit", true) { progress -> + progress.isIndeterminate = true + progress.text = "Applying edit: $instruction" + val settings = AppSettingsState.instance + val outputHumanLanguage = settings.humanLanguage + settings.getRecentCommands("customEdits").addInstructionToHistory(instruction) + val result = proxy.editCode( + state.selectedText ?: "", + instruction, + state.language?.name ?: "text", + outputHumanLanguage + ) + result.code ?: state.selectedText ?: "" + } + } catch (e: Exception) { + log.error("Failed to process edit", e) + UITools.showErrorDialog( + state.project, + "Failed to process edit: ${e.message}", + "Edit Error" + ) + state.selectedText ?: "" + } + } + + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + return event.getData(DATA_KEY) != null + } + + companion object { + private val DATA_KEY = DataKey.create("CustomEditAction.key") } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt index cf31c9c4..9cd04f74 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt @@ -2,15 +2,47 @@ package com.github.simiacryptus.aicoder.actions.code import com.github.simiacryptus.aicoder.actions.SelectionAction import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage import com.github.simiacryptus.aicoder.util.IndentedText +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.vfs.VirtualFile import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy -import com.simiacryptus.util.StringUtil + +// ... keep existing imports class DescribeAction : SelectionAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT + private val log = Logger.getInstance(DescribeAction::class.java) + protected fun processSelection(event: AnActionEvent, config: String?): String { + val state = getState(event) ?: throw IllegalStateException("No state available") + return processSelection(state, config) + } + + data class SelectionState( + val language: ComputerLanguage?, + val selectedText: String?, + val indent: String? + ) + + protected fun getState(event: AnActionEvent): SelectionState? { + val editor: Editor = event.getData(CommonDataKeys.EDITOR) ?: return null + val caret = editor.caretModel.primaryCaret + val file: VirtualFile? = event.getData(CommonDataKeys.VIRTUAL_FILE) + return SelectionState( + language = file?.let { ComputerLanguage.findByExtension(it.extension ?: "") }, + selectedText = if (caret.hasSelection()) editor.document.getText(TextRange(caret.selectionStart, caret.selectionEnd)) else null, + indent = UITools.getIndent(caret).toString() + ) + } + interface DescribeAction_VirtualAPI { fun describeCode( @@ -35,28 +67,41 @@ class DescribeAction : SelectionAction() { ).create() override fun getConfig(project: Project?): String { + // No configuration needed for this action return "" } - override fun processSelection(state: SelectionState, config: String?): String { - val description = proxy.describeCode( - IndentedText.fromString(state.selectedText).textBlock.toString().trim(), - state.language?.name ?: "", - AppSettingsState.instance.humanLanguage - ).text ?: "" - val wrapping = StringUtil.lineWrapping(description.trim(), 120) - val numberOfLines = wrapping.trim().split("\n").reversed().dropWhile { it.isEmpty() }.size - val commentStyle = if (numberOfLines == 1) { - state.language?.lineComment - } else { - state.language?.blockComment - } - return buildString { - append(state.indent) - append(commentStyle?.fromString(wrapping)?.withIndent(state.indent!!) ?: wrapping) - append("\n") - append(state.indent) - append(state.selectedText) + + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + val state = getState(event) + return state?.language != null && state.selectedText?.isNotBlank() == true + } + + protected fun processSelection(state: SelectionState, config: String?): String { + try { + val description = proxy.describeCode( + IndentedText.fromString(state.selectedText).textBlock.toString().trim(), + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ).text ?: throw IllegalStateException("Failed to generate description") + val wrapping = com.github.simiacryptus.aicoder.util.StringUtil.lineWrapping(description.trim(), 120) + val numberOfLines = wrapping.trim().split("\n").reversed().dropWhile { it.isEmpty() }.size + val commentStyle = if (numberOfLines == 1) { + state.language?.lineComment + } else { + state.language?.blockComment + } + return buildString { + append(state.indent) + append(commentStyle?.fromString(wrapping)?.withIndent(state.indent!!) ?: wrapping) + append("\n") + append(state.indent) + append(state.selectedText) + } + } catch (e: Exception) { + log.error("Failed to describe code", e) + throw e } } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt index ed745ff6..7372ce72 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt @@ -1,38 +1,90 @@ package com.github.simiacryptus.aicoder.actions.code +// ... keep existing imports import com.github.simiacryptus.aicoder.actions.SelectionAction import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.ComputerLanguage import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange import com.simiacryptus.jopenai.models.ChatModel import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy import org.jsoup.Jsoup import org.jsoup.nodes.Document +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.awt.Toolkit import java.awt.datatransfer.DataFlavor -import kotlin.toString +/** + * Base class for paste actions that convert clipboard content to appropriate code format + * Supports both text and HTML clipboard content with automatic language detection + */ abstract class PasteActionBase(private val model: (AppSettingsState) -> ChatModel) : SelectionAction(false) { override fun getActionUpdateThread() = ActionUpdateThread.BGT + protected fun processSelection(event: AnActionEvent, config: String?): String { + val state = SelectionState( + language = event.getData(CommonDataKeys.VIRTUAL_FILE)?.extension?.let { ComputerLanguage.findByExtension(it) }, + selectedText = getSelectedText(event.getData(CommonDataKeys.EDITOR)), + progress = ProgressManager.getInstance().progressIndicator + ) + return processSelection(state, config) + } + + private fun getSelectedText(editor: Editor?): String? { + if (editor == null) return null + val caret = editor.caretModel.primaryCaret + return if (caret.hasSelection()) { + editor.document.getText(TextRange(caret.selectionStart, caret.selectionEnd)) + } else null + } + + data class SelectionState( + val language: ComputerLanguage?, + val selectedText: String?, + val progress: ProgressIndicator? + ) + /** + * API interface for code conversion + */ interface VirtualAPI { fun convert(text: String, from_language: String, to_language: String): ConvertedText + /** + * Response class containing converted code + */ + class ConvertedText { var code: String? = null var language: String? = null } } + companion object { + private val log: Logger = LoggerFactory.getLogger(PasteActionBase::class.java) + } + + /** + * Smart paste action using more capable but slower model + */ + override fun getConfig(project: Project?): String { return "" } - override fun processSelection(state: SelectionState, config: String?): String { + + protected fun processSelection(state: SelectionState, config: String?): String { + val progress: ProgressIndicator? = state.progress + progress?.text = "Reading clipboard content..." val text = getClipboard().toString().trim() + progress?.text = "Converting code format..." return ChatProxy( VirtualAPI::class.java, api, @@ -64,13 +116,19 @@ abstract class PasteActionBase(private val model: (AppSettingsState) -> ChatMode } ?: false private fun getClipboard(): Any? { + try { val toolkit = Toolkit.getDefaultToolkit() val systemClipboard = toolkit.systemClipboard return systemClipboard.getContents(null)?.let { contents -> return when { - contents.isDataFlavorSupported(DataFlavor.selectionHtmlFlavor) -> contents.getTransferData(DataFlavor.selectionHtmlFlavor).let { scrubHtml(it.toString().trim()) } - contents.isDataFlavorSupported(DataFlavor.fragmentHtmlFlavor) -> contents.getTransferData(DataFlavor.fragmentHtmlFlavor).let { scrubHtml(it.toString().trim()) } - contents.isDataFlavorSupported(DataFlavor.allHtmlFlavor) -> contents.getTransferData(DataFlavor.allHtmlFlavor).let { scrubHtml(it.toString().trim()) } + contents.isDataFlavorSupported(DataFlavor.selectionHtmlFlavor) -> contents.getTransferData(DataFlavor.selectionHtmlFlavor).toString().trim() + ?.let { scrubHtml(it) } + + contents.isDataFlavorSupported(DataFlavor.fragmentHtmlFlavor) -> contents.getTransferData(DataFlavor.fragmentHtmlFlavor).toString().trim() + ?.let { scrubHtml(it) } + + contents.isDataFlavorSupported(DataFlavor.allHtmlFlavor) -> contents.getTransferData(DataFlavor.allHtmlFlavor).toString().trim() + ?.let { scrubHtml(it) } contents.isDataFlavorSupported(DataFlavor.stringFlavor) -> contents.getTransferData(DataFlavor.stringFlavor) contents.isDataFlavorSupported(DataFlavor.getTextPlainUnicodeFlavor()) -> contents.getTransferData( DataFlavor.getTextPlainUnicodeFlavor() @@ -79,6 +137,10 @@ abstract class PasteActionBase(private val model: (AppSettingsState) -> ChatMode else -> null } } + } catch (e: Exception) { + log.error("Failed to access clipboard", e) + return null + } } protected open fun scrubHtml(str: String, maxLength: Int = 100 * 1024): String { @@ -141,4 +203,14 @@ abstract class PasteActionBase(private val model: (AppSettingsState) -> ChatMode } class SmartPasteAction : PasteActionBase({ it.smartModel.chatModel() }) -class FastPasteAction : PasteActionBase({ it.fastModel.chatModel() }) \ No newline at end of file + +/** + * Fast paste action using faster but simpler model + */ +class FastPasteAction : PasteActionBase({ it.fastModel.chatModel() }) { + companion object { + private val logger: Logger = LoggerFactory.getLogger(FastPasteAction::class.java) + } + + protected var progress: ProgressIndicator? = null +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt index 6810c357..acb2eb66 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LineFilterChatAction.kt @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.actions.dev +package com.github.simiacryptus.aicoder.actions.dev import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.BaseAction @@ -6,76 +6,104 @@ import com.github.simiacryptus.aicoder.actions.generic.SessionProxyServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.skyenet.core.platform.ApplicationServices import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.model.User -import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown import com.simiacryptus.skyenet.webui.application.ApplicationServer import com.simiacryptus.skyenet.webui.chat.ChatSocketManager import com.simiacryptus.skyenet.webui.session.SessionTask import org.slf4j.LoggerFactory class LineFilterChatAction : BaseAction() { + private lateinit var lines: List + + data class ChatConfig( + val language: String, + val filename: String, + val code: String, + val lines: Array + ) + override fun getActionUpdateThread() = ActionUpdateThread.BGT val path = "/codeChat" + fun canWrite(user: String?): Boolean { + return true + } override fun handle(e: AnActionEvent) { val editor = e.getData(CommonDataKeys.EDITOR) ?: return val session = Session.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) -> + val config = getConfig(e, editor) ?: run { + UITools.error(log, "Error", Exception("Could not get required configuration")) + return + } + try { + setupChatManager(session, config) + openBrowser(e.project, session) + } catch (ex: Throwable) { + log.error("Error setting up chat", ex) + UITools.error(log, "Error", ex) + } + } + + private fun getConfig(e: AnActionEvent, editor: Editor): ChatConfig? { + return try { + val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return null + val filename = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: return null + val code = editor.caretModel.primaryCaret.selectedText ?: editor.document.text + val lines = code.split("\n").toTypedArray() + ChatConfig(language, filename, code, lines) + } catch (e: Exception) { + log.error("Error getting configuration", e) + null + } + } + + private fun setupChatManager(session: Session, config: ChatConfig) { + val codelines = config.lines.withIndex().joinToString("\n") { (i, line) -> "${i.toString().padStart(3, '0')} $line" } + val userPrompt = buildString { + append("# `${config.filename}`\n\n") + append("```${config.language}\n") + append(config.code) + append("\n```") + }.trimMargin().trim() + val systemPrompt = buildString { + append("You are a helpful AI that helps people with coding.\n\n") + append("You will be answering questions about the following code located in `${config.filename}`:\n\n") + append("```${config.language}\n") + append(codelines) + append("\n```\n\n") + append("Responses may use markdown formatting. Lines from the prompt can be included ") + append("by using the line number in a response line (e.g. `\\nLINE\\n`).\n\n") + append("For example:\n\n") + append("```text\n") + append("001\n## Injected subtitle\n\n025\n026\n\n013\n014\n") + append("```") + }.trimMargin() SessionProxyServer.agents[session] = object : ChatSocketManager( session = session, model = AppSettingsState.instance.smartModel.chatModel(), - userInterfacePrompt = """ - |# `$filename` - | - |```$language - |${code.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} - |``` - """.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.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} - |``` - | - |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(), + userInterfacePrompt = userPrompt, + systemPrompt = systemPrompt, api = api, applicationClass = ApplicationServer::class.java, - storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome), + storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome) ) { override fun canWrite(user: User?): Boolean = true override fun renderResponse(response: String, task: SessionTask): String { - return renderMarkdown(response.split("\n").joinToString("\n") { + return com.simiacryptus.skyenet.util.MarkdownUtil.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()] @@ -86,19 +114,22 @@ class LineFilterChatAction : BaseAction() { ) } } + } + + private fun openBrowser(project: Project?, session: Session) { - val server = AppServer.getServer(e.project) + val server = AppServer.getServer(project) - Thread { + ApplicationManager.getApplication().executeOnPooledThread { Thread.sleep(500) try { val uri = server.server.uri.resolve("/#$session") BaseAction.log.info("Opening browser to $uri") browse(uri) - } catch (e: Throwable) { - log.warn("Error opening browser", e) + } catch (ex: Throwable) { + log.warn("Error opening browser", ex) } - }.start() + } } override fun isEnabled(event: AnActionEvent) = AppSettingsState.instance.devActions @@ -107,4 +138,4 @@ class LineFilterChatAction : BaseAction() { private val log = LoggerFactory.getLogger(LineFilterChatAction::class.java) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt index 70497435..42dbede6 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt @@ -2,9 +2,11 @@ import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools import com.github.simiacryptus.aicoder.util.psi.PsiUtil import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager import org.slf4j.LoggerFactory /** @@ -13,19 +15,41 @@ import org.slf4j.LoggerFactory * Then, open the file you want to print the tree structure of. * Finally, select the "PrintTreeAction" action from the editor context menu. * This will print the tree structure of the file to the log. + * + * @property log Logger instance for this class + * @see BaseAction + * @see PsiUtil */ class PrintTreeAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun handle(e: AnActionEvent) { - log.warn(PsiUtil.printTree(PsiUtil.getLargestContainedEntity(e)!!)) + val project = e.project ?: return + UITools.run(project, "Analyzing Code Structure", true) { progress -> + try { + progress.isIndeterminate = true + progress.text = "Generating PSI tree structure..." + ApplicationManager.getApplication().executeOnPooledThread { + val psiEntity = PsiUtil.getLargestContainedEntity(e) + if (psiEntity != null) { + log.info(PsiUtil.printTree(psiEntity)) + } else { + log.warn("No PSI entity found in current context") + } + } + } catch (ex: Throwable) { + UITools.error(log, "Failed to print PSI tree", ex) + } + } } override fun isEnabled(event: AnActionEvent): Boolean { - return AppSettingsState.instance.devActions + if (!super.isEnabled(event)) return false + if (!AppSettingsState.instance.devActions) return false + return PsiUtil.getLargestContainedEntity(event) != null } companion object { private val log = LoggerFactory.getLogger(PrintTreeAction::class.java) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/BaseAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/BaseAction.kt new file mode 100644 index 00000000..ddcd0ed7 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/BaseAction.kt @@ -0,0 +1,28 @@ +package com.github.simiacryptus.aicoder.actions.generic + + import com.intellij.openapi.project.Project + import com.intellij.openapi.application.ApplicationManager + import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.ui.Messages +import com.simiacryptus.jopenai.ChatClient + + abstract class BaseAction : AnAction() { + protected val api: ChatClient by lazy { + ChatClient() + } + + protected fun showError(project: Project?, message: String) { + Messages.showErrorDialog(project, message, "Error") + } + + protected fun showWarning(project: Project?, message: String) { + Messages.showWarningDialog(project, message, "Warning") + } + + protected fun runWriteAction(project: Project, action: () -> Unit) { + WriteCommandAction.runWriteCommandAction(project) { + action() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt index 022bf0d2..000a4364 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CommandAutofixAction.kt @@ -11,6 +11,7 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.vfs.VirtualFile import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.skyenet.apps.general.CmdPatchApp import com.simiacryptus.skyenet.apps.general.PatchApp @@ -25,18 +26,46 @@ import javax.swing.* import kotlin.collections.set class CommandAutofixAction : BaseAction() { + private lateinit var event: AnActionEvent + + /** + * Returns the thread that should be used for action update. + */ override fun getActionUpdateThread() = ActionUpdateThread.BGT + /** + * Handles the action execution. + * Shows settings dialog, creates patch app session and opens browser interface. + */ + override fun handle(event: AnActionEvent) { - val settings = getUserSettings(event) ?: return - val dataContext = event.dataContext - val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) + try { + UITools.run(event.project, "Initializing Command Autofix", true) { progress -> + progress.isIndeterminate = true + progress.text = "Getting settings..." + val settings = getUserSettings(event) ?: return@run + val dataContext = event.dataContext + val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) + setupAndLaunchSession(event, settings, virtualFiles) + } + } catch (e: Throwable) { + log.error("Failed to execute command autofix", e) + UITools.showErrorDialog(event.project, "Failed to execute command autofix: ${e.message}", "Error") + } + } val folder = UITools.getSelectedFolder(event) val root = if (null != folder) { folder.toFile.toPath() } else { event.project?.basePath?.let { File(it).toPath() } }!! + + /** + * Sets up and launches the patch app session + */ + private fun setupAndLaunchSession(event: AnActionEvent, settings: PatchApp.Settings, virtualFiles: Array?) { + val project = event.project ?: return + val session = Session.newGlobalID() val patchApp = CmdPatchApp( root, @@ -67,7 +96,15 @@ class CommandAutofixAction : BaseAction() { }.start() } - override fun isEnabled(event: AnActionEvent) = true + /** + * Checks if the action should be enabled + */ + override fun isEnabled(event: AnActionEvent): Boolean { + if (event.project == null) return false + val folder = UITools.getSelectedFolder(event) + val hasBasePath = event.project?.basePath != null + return folder != null || hasBasePath + } companion object { private val log = LoggerFactory.getLogger(CommandAutofixAction::class.java) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileFromDescriptionAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileFromDescriptionAction.kt index e09e93a8..02baad87 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileFromDescriptionAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileFromDescriptionAction.kt @@ -6,6 +6,8 @@ import com.github.simiacryptus.aicoder.config.Name import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.ApiModel.* @@ -16,6 +18,7 @@ import javax.swing.JTextArea class CreateFileFromDescriptionAction : FileContextAction(false, true) { override fun getActionUpdateThread() = ActionUpdateThread.BGT + private val log = Logger.getInstance(CreateFileFromDescriptionAction::class.java) class ProjectFile(var path: String = "", var code: String = "") @@ -49,6 +52,26 @@ class CreateFileFromDescriptionAction : FileContextAction { + progress.isIndeterminate = false + progress.text = "Generating file from description..." + return try { + processSelectionInternal(state, config, progress) + } catch (e: Exception) { + log.error("Failed to create file from description", e) + UITools.showErrorDialog( + config?.project, + "Failed to create file: ${e.message}", + "Error" + ) + emptyArray() + } + } + + private fun processSelectionInternal( + state: SelectionState, + config: Settings?, + progress: ProgressIndicator ): Array { val projectRoot = state.projectRoot.toPath() val inputPath = projectRoot.relativize(state.selectedFile.toPath()).toString() @@ -56,6 +79,8 @@ class CreateFileFromDescriptionAction : FileContextAction(null) val codeFiles: MutableSet = mutableSetOf() + UITools.run(event.project, "Creating Image", true) { progress -> + try { + progress.text = "Analyzing code files..." - fun codeSummary() = codeFiles.filter { - root!!.resolve(it).toFile().exists() - }.associateWith { root!!.resolve(it).toFile().readText(Charsets.UTF_8) } - .entries.joinToString("\n\n") { (path, code) -> - val extension = path.toString().split('.').lastOrNull()?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } - """ - |# $path - |```$extension - |${code.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} - |``` - """.trimMargin() - } + fun codeSummary() = codeFiles.filter { + rootRef.get()?.resolve(it)?.toFile()?.exists() ?: false + }.associateWith { rootRef.get()?.resolve(it)?.toFile()?.readText(Charsets.UTF_8) } + .entries.joinToString("\n\n") { (path, code) -> + val extension = path.toString().split('.').lastOrNull()?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } + """ +|# $path +|```$extension +|${code.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} +|``` +""".trimMargin() + } - val dataContext = event.dataContext - val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) - val folder = UITools.getSelectedFolder(event) - root = if (null != folder) { - folder.toFile.toPath() - } else if (1 == virtualFiles?.size) { - UITools.getSelectedFile(event)?.parent?.toNioPath() - } else { - getModuleRootForFile(UITools.getSelectedFile(event)?.parent?.toFile ?: throw RuntimeException("")).toPath() - } + val dataContext = event.dataContext + val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) + progress.text = "Determining root directory..." + val folder = UITools.getSelectedFolder(event) + rootRef.set( + if (null != folder) { + folder.toFile.toPath() + } else if (1 == virtualFiles?.size) { + UITools.getSelectedFile(event)?.parent?.toNioPath() + } else { + getModuleRootForFile(UITools.getSelectedFile(event)?.parent?.toFile ?: throw RuntimeException("")).toPath() + } + ) + progress.text = "Collecting files..." - val files = getFiles(virtualFiles, root!!) - codeFiles.addAll(files) + val root = rootRef.get() ?: throw RuntimeException("Root path not set") + val files = getFiles(virtualFiles, root) + codeFiles.addAll(files) + progress.text = "Initializing session..." - val session = Session.newGlobalID() -// val storage = ApplicationServices.dataStorageFactory(root?.toFile()!!) as DataStorage? -// val selectedFile = UITools.getSelectedFolder(event) - if (/*null != storage &&*/ null != root) { - DataStorage.sessionPaths[session] = root.toFile()!! - } + val session = Session.newGlobalID() + DataStorage.sessionPaths[session] = root.toFile() + progress.text = "Starting server..." - SessionProxyServer.chats[session] = PatchApp(event, root.toFile(), ::codeSummary) - ApplicationServer.appInfoMap[session] = AppInfoData( - applicationName = "Code Chat", - singleInput = true, - stickyInput = false, - loadImages = false, - showMenubar = false - ) + SessionProxyServer.chats[session] = PatchApp(event, root.toFile(), ::codeSummary) + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "Code Chat", + singleInput = true, + stickyInput = false, + loadImages = false, + showMenubar = false + ) - val server = AppServer.getServer(event.project) + val server = AppServer.getServer(event.project) - Thread { - Thread.sleep(500) - try { + Thread { + Thread.sleep(500) + try { - val uri = server.server.uri.resolve("/#$session") - BaseAction.log.info("Opening browser to $uri") - browse(uri) + val uri = server.server.uri.resolve("/#$session") + BaseAction.log.info("Opening browser to $uri") + browse(uri) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() } catch (e: Throwable) { - log.warn("Error opening browser", e) + log.error("Failed to create image", e) + UITools.showErrorDialog(event.project, "Failed to create image: ${e.message}", "Error") } - }.start() + } } inner class PatchApp( @@ -156,13 +175,13 @@ class CreateImageAction : BaseAction() { actorMap: Map> = mapOf( ActorTypes.MainActor to ImageActor( prompt = """ - |You are a technical drawing assistant. - | - |You will be composing an image about the following code: - | - |${codeSummary()} - | - """.trimMargin(), +|You are a technical drawing assistant. +| +|You will be composing an image about the following code: +| +|${codeSummary()} +| +""".trimMargin(), textModel = model, imageModel = AppSettingsState.instance.mainImageModel.imageModel() ), @@ -219,15 +238,20 @@ class CreateImageAction : BaseAction() { private fun write( code: ImageResponse, path: Path - ): ByteArray { - val byteArrayOutputStream = ByteArrayOutputStream() - val data = ImageIO.write( - code.image, - path.toString().split(".").last(), - byteArrayOutputStream - ) - val bytes = byteArrayOutputStream.toByteArray() - return bytes + ): ByteArray = try { + path.parent?.toFile()?.mkdirs() + ByteArrayOutputStream().use { outputStream -> + if (!ImageIO.write( + code.image, path.toString().split(".").last(), outputStream + ) + ) { + throw IOException("Failed to write image in format: ${path.toString().split(".").last()}") + } + outputStream.toByteArray() + } + } catch (e: Exception) { + log.error("Failed to write image to $path", e) + throw RuntimeException("Failed to write image: ${e.message}", e) } private fun getFiles( @@ -246,9 +270,16 @@ class CreateImageAction : BaseAction() { return codeFiles } - override fun isEnabled(event: AnActionEvent) = true + override fun isEnabled(event: AnActionEvent): Boolean { + val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(event.dataContext) + if (virtualFiles == null || virtualFiles.isEmpty()) return false + if (event.project == null) return false +// Check if any files are selected or if a directory is selected + return virtualFiles.any { it.isDirectory || it.extension in supportedExtensions } + } companion object { private val log = LoggerFactory.getLogger(CreateImageAction::class.java) + private val supportedExtensions = setOf("kt", "java", "py", "js", "ts", "html", "css", "xml") } -} +} \ No newline at end of file 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 121f9794..258bc6ae 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 @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.actions.generic +package com.github.simiacryptus.aicoder.actions.generic import com.github.simiacryptus.aicoder.AppServer import com.github.simiacryptus.aicoder.actions.BaseAction @@ -6,10 +6,13 @@ import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.CodeChatSocketManager import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.util.TextRange import com.simiacryptus.diff.addApplyDiffLinks @@ -23,32 +26,60 @@ import com.simiacryptus.skyenet.webui.application.ApplicationServer import com.simiacryptus.skyenet.webui.session.SessionTask import org.intellij.lang.annotations.Language import org.slf4j.LoggerFactory +import com.intellij.openapi.application.ApplicationManager as IntellijAppManager class DiffChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT val path = "/diffChat" + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + val editor = event.getData(CommonDataKeys.EDITOR) ?: return false + val document = editor.document + return FileDocumentManager.getInstance().getFile(document) != null + } + override fun handle(e: AnActionEvent) { + try { val editor = e.getData(CommonDataKeys.EDITOR) ?: return - val session = Session.newGlobalID() + val session = Session.newGlobalID() val language = ComputerLanguage.getComputerLanguage(e)?.name ?: "" val document = editor.document val filename = FileDocumentManager.getInstance().getFile(document)?.name ?: return + val (rawText, selectionStart, selectionEnd) = getSelectionDetails(editor) + UITools.run(e.project, "Initializing Chat", true) { progress -> + progress.isIndeterminate = true + progress.text = "Setting up chat session..." + setupApplicationServer(session) + setupSessionProxy(session, language, rawText, filename, editor, selectionStart, selectionEnd, document) + openBrowserWindow(e, session) + } + } catch (ex: Throwable) { + log.error("Error in DiffChat action", ex) + UITools.showErrorDialog(e.project, "Failed to initialize chat: ${ex.message}", "Error") + } + } + + private fun getSelectionDetails(editor: Editor): Triple { val primaryCaret = editor.caretModel.primaryCaret - val rawText: String - val selectionStart: Int - var selectionEnd: Int val selectedText = primaryCaret.selectedText - if (null != selectedText) { - rawText = selectedText - selectionStart = primaryCaret.selectionStart - selectionEnd = primaryCaret.selectionEnd + return if (selectedText != null) { + Triple( + selectedText.toString(), + primaryCaret.selectionStart, + primaryCaret.selectionEnd + ) } else { - rawText = document.text - selectionStart = 0 - selectionEnd = rawText.length + Triple( + editor.document.text, + 0, + editor.document.text.length + ) } + } + + private fun setupApplicationServer(session: Session) { ApplicationServer.appInfoMap[session] = AppInfoData( applicationName = "Code Chat", singleInput = false, @@ -56,6 +87,20 @@ class DiffChatAction : BaseAction() { loadImages = false, showMenubar = false ) + } + + private fun setupSessionProxy( + session: Session, + language: String, + rawText: String, + filename: String, + editor: Editor, + selectionStart: Int, + selectionEnd: Int, + document: Document + ) { + var selectionEnd = selectionEnd + SessionProxyServer.agents[session] = object : CodeChatSocketManager( session = session, language = language, @@ -65,6 +110,8 @@ class DiffChatAction : BaseAction() { model = AppSettingsState.instance.smartModel.chatModel(), storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome) ) { + + // ... rest of the implementation override val systemPrompt: String @Language("Markdown") get() = super.systemPrompt + """ @@ -106,9 +153,8 @@ class DiffChatAction : BaseAction() { |``` """.trimMargin() - val ui by lazy { ApplicationInterface(this) } - override fun renderResponse(response: String, task: SessionTask) = """
${ + override fun renderResponse(response: String, task: SessionTask): String = """
${ renderMarkdown( addApplyDiffLinks( code = { @@ -116,7 +162,7 @@ class DiffChatAction : BaseAction() { }, response = response, handle = { newCode: String -> - WriteCommandAction.runWriteCommandAction(e.project) { + WriteCommandAction.runWriteCommandAction(editor.project) { selectionEnd = selectionStart + newCode.length document.replaceString(selectionStart, selectionStart + rawText.length, newCode) } @@ -127,25 +173,20 @@ class DiffChatAction : BaseAction() { ) }
""" } + } - val server = AppServer.getServer(e.project) - - Thread { - Thread.sleep(500) - try { - val uri = server.server.uri.resolve("/#$session") - BaseAction.log.info("Opening browser to $uri") - browse(uri) - } catch (e: Throwable) { - log.warn("Error opening browser", e) - } - }.start() + private fun openBrowserWindow(e: AnActionEvent, session: Session) { + IntellijAppManager.getApplication().executeOnPooledThread { + val server = AppServer.getServer(e.project) + val uri = server.server.uri.resolve("/#$session") + BaseAction.log.info("Opening browser to $uri") + browse(uri) + } } - override fun isEnabled(event: AnActionEvent) = true companion object { private val log = LoggerFactory.getLogger(DiffChatAction::class.java) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchAction.kt index c7d25389..1cfd6a6c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchAction.kt @@ -85,13 +85,14 @@ class DocumentedMassPatchAction : BaseAction() { private fun getConfig(project: Project?, e: AnActionEvent): Settings? { var root = UITools.getSelectedFolder(e)?.toNioPath() - val allFiles = root?.let { Files.walk(it).toList() } ?: UITools.getSelectedFiles(e).map { it.toNioPath() } + val allFiles: List = root?.let { Files.walk(it).toList() } + ?: UITools.getSelectedFiles(e).map { it.toNioPath() } if (root == null) { root = e.project?.basePath?.let { File(it).toPath() } } - val docFiles = allFiles.filter { it.toString().endsWith(".md") }.toTypedArray() - val sourceFiles = allFiles.filter { - isLLMIncludableFile(it.toFile()) && !it.toString().endsWith(".md") + val docFiles: Array = allFiles.filter { it.toString().endsWith(".md") }.toTypedArray() + val sourceFiles: Array = allFiles.filter { + isLLMIncludableFile(it.toFile()) && !it.toString().endsWith(".md") }.toTypedArray() val settingsUI = SettingsUI().apply { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchServer.kt index 4d7416f8..c0d04e45 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchServer.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentedMassPatchServer.kt @@ -25,6 +25,11 @@ class DocumentedMassPatchServer( val config: DocumentedMassPatchAction.Settings, val api: ChatClient, val autoApply: Boolean + /** + * Server for handling documented mass code patches + * @param config Settings containing project and file configurations + * @param api ChatClient for AI interactions + * @param autoApply Whether to automatically apply suggested patches */ ) : ApplicationServer( applicationName = "Documented Code Patch", path = "/patchChat", @@ -35,6 +40,10 @@ class DocumentedMassPatchServer( override val singleInput = false override val stickyInput = true + /** + * Main actor for processing code reviews and generating patches + */ + private val mainActor: SimpleActor get() { return SimpleActor( @@ -54,6 +63,13 @@ class DocumentedMassPatchServer( ) } + /** + * Creates a new session for handling code review and patch generation + * @param user The user initiating the session + * @param session The session context + * @return SocketManager for managing the session + */ + override fun newSession(user: User?, session: Session): SocketManager { val socketManager = super.newSession(user, session) val ui = (socketManager as ApplicationSocketManager).applicationInterface @@ -61,8 +77,10 @@ class DocumentedMassPatchServer( val task = ui.newTask(true) val api = (api as ChatClient).getChildClient().apply { val createFile = task.createFile(".logs/api-${UUID.randomUUID()}.log") + // Handle potential null from createFile createFile.second?.apply { logStreams += this.outputStream().buffered() + task.add("Initializing API logging...") task.verbose("API log: $this") } } @@ -85,6 +103,7 @@ class DocumentedMassPatchServer( socketManager.scheduledThreadPoolExecutor.schedule({ socketManager.pool.submit { try { + task.add("Processing ${path}...") val codeSummary = """ $docSummary @@ -138,6 +157,7 @@ class DocumentedMassPatchServer( atomicRef = AtomicReference(), semaphore = Semaphore(0), ).call() + task.add("Completed processing ${path}") } catch (e: Exception) { log.warn("Error processing $path", e) task.error(ui, e) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt index 8be7b82c..1a251f15 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/GenerateRelatedFileAction.kt @@ -7,6 +7,7 @@ import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project @@ -27,6 +28,8 @@ import java.util.concurrent.atomic.AtomicReference import javax.swing.JTextArea class GenerateRelatedFileAction : FileContextAction() { + private val log = Logger.getInstance(GenerateRelatedFileAction::class.java) + override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun isEnabled(event: AnActionEvent): Boolean { return UITools.getSelectedFiles(event).size == 1 && super.isEnabled(event) @@ -69,31 +72,46 @@ class GenerateRelatedFileAction : FileContextAction { - val root = getModuleRootForFile(state.selectedFile).toPath() - val selectedFile = state.selectedFile - val analogue = generateFile( - ProjectFile( - path = root.relativize(selectedFile.toPath()).toString(), - code = IOUtils.toString(FileInputStream(selectedFile), "UTF-8") - ), - config?.settings?.directive ?: "" - ) - var outputPath = root.resolve(analogue.path) - if (outputPath.toFile().exists()) { - val extension = outputPath.toString().split(".").last() - val name = outputPath.toString().split(".").dropLast(1).joinToString(".") - val fileIndex = (1..Int.MAX_VALUE).find { - !root.resolve("$name.$it.$extension").toFile().exists() + try { + progress.isIndeterminate = false + progress.text = "Reading source file..." + progress.fraction = 0.2 + val root = getModuleRootForFile(state.selectedFile).toPath() + val selectedFile = state.selectedFile + val analogue = generateFile( + baseFile = ProjectFile( + path = root.relativize(selectedFile.toPath()).toString(), + code = IOUtils.toString(FileInputStream(selectedFile), "UTF-8") + ), + directive = config?.settings?.directive ?: "", + progress = progress + ) + progress.text = "Generating output file..." + progress.fraction = 0.6 + var outputPath = root.resolve(analogue.path) + if (outputPath.toFile().exists()) { + val extension = outputPath.toString().split(".").last() + val name = outputPath.toString().split(".").dropLast(1).joinToString(".") + val fileIndex = (1..Int.MAX_VALUE).find { + !root.resolve("$name.$it.$extension").toFile().exists() + } + outputPath = root.resolve("$name.$fileIndex.$extension") } - outputPath = root.resolve("$name.$fileIndex.$extension") + progress.text = "Writing output file..." + progress.fraction = 0.8 + outputPath.parent.toFile().mkdirs() + FileUtils.write(outputPath.toFile(), analogue.code, "UTF-8") + open(config?.project!!, outputPath) + return arrayOf(outputPath.toFile()) + } catch (e: Exception) { + log.error("Failed to generate related file", e) + throw e } - outputPath.parent.toFile().mkdirs() - FileUtils.write(outputPath.toFile(), analogue.code, "UTF-8") - open(config?.project!!, outputPath) - return arrayOf(outputPath.toFile()) } - private fun generateFile(baseFile: ProjectFile, directive: String): ProjectFile { + private fun generateFile(baseFile: ProjectFile, directive: String, progress: ProgressIndicator): ProjectFile = try { + progress.text = "Generating content with AI..." + progress.fraction = 0.4 val model = AppSettingsState.instance.smartModel.chatModel() val chatRequest = ApiModel.ChatRequest( model = model.modelName, @@ -110,7 +128,7 @@ class GenerateRelatedFileAction : FileContextAction + progress.isIndeterminate = true + progress.text = "Setting up chat session..." - Thread { - Thread.sleep(500) - try { + val session = Session.newGlobalID() + SessionProxyServer.agents[session] = ChatSocketManager( + session = session, + model = model, + initialAssistantPrompt = "", + userInterfacePrompt = userInterfacePrompt, + systemPrompt = systemPrompt, + api = api, + storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome), + applicationClass = ApplicationServer::class.java, + ) + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "Code Chat", + singleInput = false, + stickyInput = true, + loadImages = false, + showMenubar = false + ) + val server = AppServer.getServer(project) val uri = server.server.uri.resolve("/#$session") - BaseAction.log.info("Opening browser to $uri") - browse(uri) - } catch (e: Throwable) { - log.warn("Error opening browser", e) + ApplicationManager.getApplication().executeOnPooledThread { + try { + BaseAction.log.info("Opening browser to $uri") + browse(uri) + } catch (e: Throwable) { + UITools.error(log, "Failed to open browser", e) + } + } } - }.start() + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } } override fun isEnabled(event: AnActionEvent) = true + fun updateAction(e: AnActionEvent) { + e.presentation.isEnabled = e.project != null + } + companion object { - private val log = LoggerFactory.getLogger(CodeChatAction::class.java) + private val log = LoggerFactory.getLogger(GenericChatAction::class.java) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiCodeChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiCodeChatAction.kt index c7e8cb8c..55f60b75 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiCodeChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiCodeChatAction.kt @@ -27,7 +27,15 @@ import com.simiacryptus.skyenet.webui.application.ApplicationServer import org.slf4j.LoggerFactory import java.io.File import java.nio.file.Path -import java.util.UUID +import java.util.* + +/** + * Action that enables multi-file code chat functionality. + * Allows users to select multiple files and discuss them with an AI assistant. + * Supports code modifications through patch application. + * + * @see BaseAction + */ class MultiCodeChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -41,9 +49,9 @@ class MultiCodeChatAction : BaseAction() { .entries.joinToString("\n\n") { (path, code) -> val extension = path.toString().split('.').lastOrNull()?.let { /*escapeHtml4*/(it)/*.indent(" ")*/ } """ - |# $path - |```$extension - |${code} + # $path + ```$extension + ${code} |``` """.trimMargin() } @@ -59,16 +67,28 @@ class MultiCodeChatAction : BaseAction() { val files = getFiles(virtualFiles, root!!) codeFiles.addAll(files) - val session = Session.newGlobalID() - SessionProxyServer.chats[session] = PatchApp(root.toFile(), { codeSummary() }, codeFiles) - ApplicationServer.appInfoMap[session] = AppInfoData( - applicationName = "Code Chat", - singleInput = false, - stickyInput = true, - loadImages = false, - showMenubar = false - ) - val server = AppServer.getServer(event.project) + try { + UITools.run(event.project, "Initializing Chat", true) { progress -> + progress.isIndeterminate = true + progress.text = "Setting up chat session..." + val session = Session.newGlobalID() + SessionProxyServer.chats[session] = PatchApp(root.toFile(), { codeSummary() }, codeFiles) + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "Code Chat", + singleInput = false, + stickyInput = true, + loadImages = false, + showMenubar = false + ) + val server = AppServer.getServer(event.project) + launchBrowser(server, session.toString()) + } + } catch (e: Throwable) { + UITools.error(log, "Failed to initialize chat session", e) + } + } + + private fun launchBrowser(server: AppServer, session: String) { Thread { Thread.sleep(500) @@ -83,7 +103,14 @@ class MultiCodeChatAction : BaseAction() { }.start() } - inner class PatchApp( + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + val files = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(event.dataContext) + return files != null && files.isNotEmpty() + } + + /** Application class that handles the chat interface and code modifications */ + private inner class PatchApp( override val root: File, val codeSummary: () -> String, val codeFiles: Set = setOf(), @@ -94,6 +121,7 @@ class MultiCodeChatAction : BaseAction() { ) { override val singleInput = false override val stickyInput = true + private val mainActor: SimpleActor get() = SimpleActor( prompt = """ @@ -107,6 +135,13 @@ class MultiCodeChatAction : BaseAction() { model = AppSettingsState.instance.smartModel.chatModel() ) + /** + * Handles user messages in the chat interface + * + * @throws RuntimeException if API calls fail + * @throws IOException if file operations fail + */ + override fun userMessage( session: Session, user: User?, @@ -164,14 +199,22 @@ class MultiCodeChatAction : BaseAction() { } } + /** + * Recursively collects files from the selected virtual files + * + * @param virtualFiles Array of selected virtual files + * @param root Project root path + * @return Set of relative paths to the selected files + */ + private fun getFiles( virtualFiles: Array?, root: Path - ): MutableSet { + ): Set { val codeFiles = mutableSetOf() virtualFiles?.forEach { file -> - if (file.isDirectory) { + if (file.isDirectory && !file.name.startsWith(".")) { getFiles(file.children, root) } else { codeFiles.add(root.relativize(file.toNioPath())) @@ -180,7 +223,6 @@ class MultiCodeChatAction : BaseAction() { return codeFiles } - override fun isEnabled(event: AnActionEvent) = true companion object { private val log = LoggerFactory.getLogger(MultiDiffChatAction::class.java) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiDiffChatAction.kt index d41b0efe..dd51c6c3 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 @@ -36,7 +36,11 @@ import java.util.concurrent.atomic.AtomicReference class MultiDiffChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT -// Add proper error handling per best practices + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(event.dataContext) + return virtualFiles != null && virtualFiles.isNotEmpty() + } override fun handle(event: AnActionEvent) { try { @@ -48,10 +52,12 @@ class MultiDiffChatAction : BaseAction() { root = if (null != folder) { folder.toFile.toPath() } else { - getModuleRootForFile(UITools.getSelectedFile(event)?.parent?.toFile ?: throw RuntimeException("")).toPath() + getModuleRootForFile( + UITools.getSelectedFile(event)?.parent?.toFile + ?: throw RuntimeException("No file or folder selected") + ).toPath() } val initialFiles = getFiles(virtualFiles, root) - val session = Session.newGlobalID() SessionProxyServer.chats[session] = PatchApp(root.toFile(), initialFiles) ApplicationServer.appInfoMap[session] = AppInfoData( @@ -62,8 +68,6 @@ class MultiDiffChatAction : BaseAction() { showMenubar = false ) val server = AppServer.getServer(event.project) - -// Use proper thread management with progress indication UITools.run(event.project, "Opening Browser", true) { progress -> Thread.sleep(500) try { @@ -71,11 +75,13 @@ class MultiDiffChatAction : BaseAction() { BaseAction.log.info("Opening browser to $uri") browse(uri) } catch (e: Throwable) { - log.warn("Error opening browser", e) - UITools.showErrorDialog(event.project, "Failed to open browser: ${e.message}", "Error") + val message = "Failed to open browser: ${e.message}" + log.error(message, e) + UITools.showErrorDialog(event.project, message, "Error") } } } catch (e: Exception) { + // Comprehensive error logging log.error("Error in MultiDiffChatAction", e) UITools.showErrorDialog(event.project, e.message ?: "", "Error") } @@ -101,7 +107,12 @@ class MultiDiffChatAction : BaseAction() { log.warn("Root directory does not exist: $root") return emptySet() } - return initialFiles.filter { root.toPath().resolve(it).toFile().exists() }.toSet() + return initialFiles.filter { path -> + val file = root.toPath().resolve(path).toFile() + val exists = file.exists() + if (!exists) log.warn("File does not exist: $file") + exists + }.toSet() } private fun codeSummary(): String { @@ -237,17 +248,26 @@ ${code.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} root: Path ): MutableSet { val codeFiles = mutableSetOf() + if (virtualFiles == null) { + log.warn("No virtual files provided") + return codeFiles + } + // Filter out unsupported file types + val supportedExtensions = setOf("kt", "java", "py", "js", "ts", "html", "css", "xml") + fun isSupportedFile(file: VirtualFile): Boolean { + return file.extension in supportedExtensions + } + virtualFiles?.forEach { file -> if (file.isDirectory) { getFiles(file.children, root) - } else { + } else if (isSupportedFile(file)) { codeFiles.add(root.relativize(file.toNioPath())) } } return codeFiles } - override fun isEnabled(event: AnActionEvent) = true companion object { private val log = LoggerFactory.getLogger(MultiDiffChatAction::class.java) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt index dff29145..4de580bb 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/MultiStepPatchAction.kt @@ -9,6 +9,7 @@ import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.application.ApplicationManager import com.simiacryptus.diff.addApplyFileDiffLinks import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.ChatClient @@ -24,7 +25,8 @@ import com.simiacryptus.skyenet.Discussable import com.simiacryptus.skyenet.Retryable import com.simiacryptus.skyenet.TabbedDisplay import com.simiacryptus.skyenet.core.actors.* -import com.simiacryptus.skyenet.core.platform.* +import com.simiacryptus.skyenet.core.platform.ApplicationServices +import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.file.DataStorage import com.simiacryptus.skyenet.core.platform.model.StorageInterface import com.simiacryptus.skyenet.core.platform.model.User @@ -44,8 +46,18 @@ class MultiStepPatchAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT val path = "/autodev" + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + val files = UITools.getSelectedFolder(event) ?: return false + return true + } + override fun handle(e: AnActionEvent) { + val project = e.project ?: return + UITools.run(project, "Initializing Auto Dev Assistant", true) { progress -> + progress.isIndeterminate = true + try { val session = Session.newGlobalID() val storage = ApplicationServices.dataStorageFactory(AppSettingsState.instance.pluginHome) as DataStorage? val selectedFile = UITools.getSelectedFolder(e) @@ -62,17 +74,17 @@ class MultiStepPatchAction : BaseAction() { ) val server = AppServer.getServer(e.project) - Thread { - Thread.sleep(500) - try { - val uri = server.server.uri.resolve("/#$session") - BaseAction.log.info("Opening browser to $uri") - browse(uri) + ApplicationManager.getApplication().invokeLater { + progress.text = "Opening browser..." + val uri = server.server.uri.resolve("/#$session") + BaseAction.log.info("Opening browser to $uri") + browse(uri) + } } catch (e: Throwable) { - log.warn("Error opening browser", e) + UITools.error(log, "Failed to initialize Auto Dev Assistant", e) } - }.start() + } } open class AutoDevApp( @@ -84,6 +96,10 @@ class MultiStepPatchAction : BaseAction() { path = "/autodev", showMenubar = false, ) { + companion object { + private const val DEFAULT_BUDGET = 2.00 + } + override fun userMessage( session: Session, user: User?, @@ -91,8 +107,11 @@ class MultiStepPatchAction : BaseAction() { ui: ApplicationInterface, api: API ) { - val settings = getSettings(session, user) ?: Settings() - if (api is ChatClient) api.budget = settings.budget ?: 2.00 + val settings = getSettings(session, user) ?: Settings( + budget = DEFAULT_BUDGET, + model = AppSettingsState.instance.smartModel.chatModel() + ) + if (api is ChatClient) api.budget = settings.budget ?: DEFAULT_BUDGET AutoDevAgent( api = api, dataStorage = dataStorage, diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ShellCommandAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ShellCommandAction.kt index 7f4c1ee0..26157e01 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ShellCommandAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ShellCommandAction.kt @@ -7,7 +7,7 @@ import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.ui.Messages +import com.intellij.openapi.progress.ProgressIndicator import com.simiacryptus.jopenai.API import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.skyenet.apps.code.CodingAgent @@ -28,12 +28,25 @@ class ShellCommandAction : BaseAction() { } override fun handle(e: AnActionEvent) { - val project = e.project - val selectedFolder = UITools.getSelectedFolder(e)?.toFile - if (selectedFolder == null) { - Messages.showErrorDialog(project, "Please select a directory", "Error") - return + val project = e.project ?: return + UITools.run(project, "Initializing Shell Command", true) { progress -> + try { + initializeShellCommand(e, progress) + } catch (ex: Exception) { + log.error("Failed to initialize shell command", ex) + UITools.showErrorDialog( + project, + "Failed to initialize shell command: ${ex.message}", + "Error" + ) + } } + } + + private fun initializeShellCommand(e: AnActionEvent, progress: ProgressIndicator) { + progress.text = "Setting up shell environment..." + val selectedFolder = UITools.getSelectedFolder(e)?.toFile ?: throw IllegalStateException("No directory selected") + progress.text = "Configuring session..." val session = Session.newGlobalID() ApplicationServer.appInfoMap[session] = AppInfoData( applicationName = "Code Chat", @@ -42,6 +55,7 @@ class ShellCommandAction : BaseAction() { loadImages = false, showMenubar = false ) + SessionProxyServer.chats[session] = object : ApplicationServer( applicationName = "Shell Agent", path = "/shellAgent", @@ -58,6 +72,7 @@ class ShellCommandAction : BaseAction() { ui: ApplicationInterface, api: API ) { + progress.text = "Processing command..." val task = ui.newTask() val agent = object : CodingAgent( api = api, @@ -74,7 +89,10 @@ class ShellCommandAction : BaseAction() { temperature = AppSettingsState.instance.temperature, details = """ Execute the following shell command(s) in the specified directory and provide the output. - Ensure to handle any errors or exceptions gracefully. + Guidelines: + - Handle errors and exceptions gracefully + - Provide clear output formatting + - Support command cancellation """.trimIndent(), model = AppSettingsState.instance.smartModel.chatModel(), mainTask = task, @@ -122,6 +140,7 @@ class ShellCommandAction : BaseAction() { } } } + progress.text = "Opening browser interface..." val server = AppServer.getServer(e.project) @@ -131,7 +150,7 @@ class ShellCommandAction : BaseAction() { val uri = server.server.uri.resolve("/#$session") log.info("Opening browser to $uri") browse(uri) - } catch (e: Throwable) { + } catch (e: Exception) { log.warn("Error opening browser", e) } }.start() @@ -140,4 +159,4 @@ class ShellCommandAction : BaseAction() { companion object { private val isWindows = System.getProperty("os.name").lowercase().contains("windows") } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt index 5958bf1e..9c6df23c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/SimpleCommandAction.kt @@ -38,38 +38,47 @@ import kotlin.io.path.walk class SimpleCommandAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT - // Add error handling wrapper for main action override fun handle(event: AnActionEvent) { + val project = event.project try { - val settings = getUserSettings(event) ?: run { - log.error("Failed to retrieve user settings.") - return - } - val dataContext = event.dataContext - val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) - val folder = UITools.getSelectedFolder(event) - val root = folder?.toFile?.toPath() ?: event.project?.basePath?.let { File(it).toPath() } ?: run { - log.error("Failed to determine project root.") - return - } + val settings = getUserSettings(event) ?: run { + log.error("Failed to retrieve user settings") + UITools.showErrorDialog(project, "Failed to retrieve settings", "Error") + return + } + UITools.run(project, "Initializing", true) { progress -> + progress.text = "Setting up command execution..." + val dataContext = event.dataContext + val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) + val folder = UITools.getSelectedFolder(event) + val root = folder?.toFile?.toPath() ?: project?.basePath?.let { File(it).toPath() } ?: run { + throw IllegalStateException("Failed to determine project root") + } - val session = Session.newGlobalID() - val patchApp = createPatchApp(root.toFile(), session, settings, virtualFiles) - SessionProxyServer.chats[session] = patchApp - ApplicationServer.appInfoMap[session] = AppInfoData( - applicationName = "Code Chat", - singleInput = true, - stickyInput = false, - loadImages = false, - showMenubar = false - ) - val server = AppServer.getServer(event.project) + val session = Session.newGlobalID() + progress.text = "Creating patch application..." + val patchApp = createPatchApp(root.toFile(), session, settings, virtualFiles) + progress.text = "Configuring session..." + SessionProxyServer.chats[session] = patchApp + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "Code Chat", + singleInput = true, + stickyInput = false, + loadImages = false, + showMenubar = false + ) + val server = AppServer.getServer(project) + openBrowserWithDelay(server.server.uri.resolve("/#$session")) + } - openBrowserWithDelay(server.server.uri.resolve("/#$session")) } catch (e: Exception) { log.error("Error handling action", e) - UITools.showErrorDialog(event.project, "Failed to execute command: ${e.message}", "Error") + UITools.showErrorDialog( + project, + "Failed to execute command: ${e.message}", + "Error" + ) } } @@ -78,13 +87,16 @@ class SimpleCommandAction : BaseAction() { session: Session, settings: Settings, virtualFiles: Array? - ): PatchApp = UITools.run(null, "Creating patch application", true) { progress -> + ): PatchApp = UITools.run(null, "Creating Patch Application", true) { progress -> progress.text = "Initializing patch application..." object : PatchApp(root, session, settings) { + // Limit file size to 0.5MB for performance + private val maxFileSize = 512 * 1024 + override fun codeFiles() = (virtualFiles?.toList()?.flatMap { FileValidationUtils.expandFileList(it.toFile).toList() }?.map { it.toPath() }?.toSet()?.toMutableSet() ?: mutableSetOf()) - .filter { it.toFile().length() < 1024 * 1024 / 2 } // Limit to 0.5MB + .filter { it.toFile().length() < maxFileSize } .map { root.toPath().relativize(it) ?: it }.toSet() // Add progress indication for long operations @@ -93,6 +105,7 @@ class SimpleCommandAction : BaseAction() { .mapIndexed { index, path -> progress.fraction = index.toDouble() / paths.size progress.text = "Processing ${path.fileName}..." + if (progress.isCanceled) throw InterruptedException("Operation cancelled") """ |# ${settings.workingDirectory.toPath()?.relativize(path)} |$tripleTilde${path.toString().split('.').lastOrNull()} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt index 6eca96a0..2339260c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevelopmentAssistantAction.kt @@ -48,27 +48,32 @@ class WebDevelopmentAssistantAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT private val path = "/webDev" + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + val file = UITools.getSelectedFile(event) ?: return false + return file.isDirectory + } + override fun handle(e: AnActionEvent) { try { - val session = Session.newGlobalID() - val selectedFile = UITools.getSelectedFolder(e) - if (null != selectedFile) { + val project = e.project ?: return + val session = Session.newGlobalID() + val selectedFile = UITools.getSelectedFolder(e) ?: return DataStorage.sessionPaths[session] = selectedFile.toFile - } - SessionProxyServer.chats[session] = WebDevApp(root = selectedFile) - ApplicationServer.appInfoMap[session] = AppInfoData( - applicationName = "Code Chat", - singleInput = true, - stickyInput = false, - loadImages = false, - showMenubar = false - ) - val server = AppServer.getServer(e.project) + SessionProxyServer.chats[session] = WebDevApp(root = selectedFile) + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "Code Chat", + singleInput = true, + stickyInput = false, + loadImages = false, + showMenubar = false + ) + val server = AppServer.getServer(project) UITools.run(e.project, "Opening Web Development Assistant", true) { progress -> progress.text = "Launching browser..." - Thread.sleep(500) + Thread.sleep(500) val uri = server.server.uri.resolve("/#$session") BaseAction.log.info("Opening browser to $uri") @@ -79,10 +84,6 @@ class WebDevelopmentAssistantAction : BaseAction() { } } - override fun isEnabled(event: AnActionEvent): Boolean { - if (UITools.getSelectedFile(event)?.isDirectory == false) return false - return super.isEnabled(event) - } open class WebDevApp( applicationName: String = "Web Development Agent", @@ -97,6 +98,10 @@ class WebDevelopmentAssistantAction : BaseAction() { ) { private val log = LoggerFactory.getLogger(WebDevApp::class.java) + companion object { + private const val DEFAULT_BUDGET = 2.00 + } + override fun userMessage( session: Session, user: User?, @@ -105,23 +110,24 @@ class WebDevelopmentAssistantAction : BaseAction() { api: API ) { try { - val settings = getSettings(session, user) ?: Settings() - if (api is ChatClient) api.budget = settings.budget ?: 2.00 - WebDevAgent( - api = api, - dataStorage = dataStorage, - session = session, - user = user, - ui = ui, - tools = settings.tools, - model = settings.model, - parsingModel = settings.parsingModel, - root = root, - ).start( - userMessage = userMessage, - ) + val settings = getSettings(session, user) ?: Settings() + if (api is ChatClient) { + api.budget = settings.budget ?: DEFAULT_BUDGET + } + WebDevAgent( + api = api, + dataStorage = dataStorage, + session = session, + user = user, + ui = ui, + tools = settings.tools, + model = settings.model, + parsingModel = settings.parsingModel, + root = root, + ).start(userMessage = userMessage) } catch (e: Throwable) { log.error("Error processing user message", e) + throw e } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ChatWithCommitDiffAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ChatWithCommitDiffAction.kt index b30ecf74..7520f024 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ChatWithCommitDiffAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ChatWithCommitDiffAction.kt @@ -1,12 +1,13 @@ package com.github.simiacryptus.aicoder.actions.git import com.github.simiacryptus.aicoder.AppServer +import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.actions.generic.SessionProxyServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.CodeChatSocketManager import com.github.simiacryptus.aicoder.util.IdeaChatClient -import com.intellij.openapi.actionSystem.AnAction +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project @@ -24,34 +25,40 @@ import com.simiacryptus.skyenet.core.platform.ApplicationServices import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.webui.application.AppInfoData import com.simiacryptus.skyenet.webui.application.ApplicationServer -import javax.swing.JOptionPane +import com.intellij.openapi.application.ApplicationManager as IntellijAppManager -class ChatWithCommitDiffAction : AnAction() { +class ChatWithCommitDiffAction : BaseAction( + name = "Chat with Commit Diff", + description = "Opens a chat interface to discuss commit differences" +) { companion object { private val log = Logger.getInstance(ChatWithCommitDiffAction::class.java) } - override fun actionPerformed(e: AnActionEvent) { + override fun handle(e: AnActionEvent) { log.info("Comparing selected commit with the current HEAD") val project = e.project ?: return val selectedCommit = e.getData(VcsDataKeys.VCS_REVISION_NUMBER) ?: return val vcsManager = ProjectLevelVcsManager.getInstance(project) val vcs = vcsManager.allActiveVcss.firstOrNull() ?: run { - JOptionPane.showMessageDialog(null, "No active VCS found", "Error", JOptionPane.ERROR_MESSAGE) + UITools.showErrorDialog(project, "No active VCS found", "Error") return } - Thread { + UITools.run(project, "Comparing Changes", true) { progress -> try { + progress.text = "Retrieving changes between commits..." val diffInfo = getChangesBetweenCommits(project, selectedCommit).ifEmpty { "No changes found" } + progress.text = "Opening chat interface..." openChatWithDiff(e, diffInfo) } catch (e: Throwable) { log.error("Error comparing changes", e) - JOptionPane.showMessageDialog(null, e.message, "Error", JOptionPane.ERROR_MESSAGE) + UITools.showErrorDialog(project, "Error comparing changes: ${e.message}", "Error") } - }.start() + } } + private fun openChatWithDiff(e: AnActionEvent, diffInfo: String) { val session = Session.newGlobalID() SessionProxyServer.agents[session] = CodeChatSocketManager( @@ -73,7 +80,7 @@ class ChatWithCommitDiffAction : AnAction() { val server = AppServer.getServer(e.project) - Thread { + IntellijAppManager.getApplication().executeOnPooledThread { Thread.sleep(500) try { val uri = server.server.uri.resolve("/#$session") @@ -82,7 +89,7 @@ class ChatWithCommitDiffAction : AnAction() { } catch (e: Throwable) { log.warn("Error opening browser", e) } - }.start() + } } private fun getChangesBetweenCommits(project: Project, selectedCommit: VcsRevisionNumber): String { @@ -136,11 +143,10 @@ class ChatWithCommitDiffAction : AnAction() { } - override fun update(e: AnActionEvent) { + fun updateAction(e: AnActionEvent) { val project = e.project e.presentation.isEnabledAndVisible = project != null && ProjectLevelVcsManager.getInstance(project).allActiveVcss.isNotEmpty() } -} - +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ReplicateCommitAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ReplicateCommitAction.kt index 65b153b6..8d3e5472 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ReplicateCommitAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/git/ReplicateCommitAction.kt @@ -11,7 +11,9 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread 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.diagnostic.Logger +import com.intellij.openapi.ui.Messages import com.intellij.openapi.vcs.VcsDataKeys import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vfs.VirtualFile @@ -44,18 +46,22 @@ class ReplicateCommitAction : BaseAction() { private val logger = Logger.getInstance(ReplicateCommitAction::class.java) override fun getActionUpdateThread() = ActionUpdateThread.BGT - // Add error handling wrapper override fun handle(event: AnActionEvent) { + val project = event.project ?: return try { - val settings = getUserSettings(event) ?: return - val dataContext = event.dataContext + val settings = getUserSettings(event) ?: run { + Messages.showErrorDialog(project, "Could not determine working directory", "Configuration Error") + return + } + + val dataContext = event.dataContext val virtualFiles = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext) val folder = UITools.getSelectedFolder(event) var root = if (null != folder) { folder.toFile.toPath() } else { - event.project?.basePath?.let { File(it).toPath() } + project.basePath?.let { File(it).toPath() } }!! val virtualFiles1 = event.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY) @@ -63,9 +69,8 @@ class ReplicateCommitAction : BaseAction() { val changes = event.getData(VcsDataKeys.CHANGES) val session = Session.newGlobalID() - UITools.run(event.project, "Replicating Commit", true) { progress -> + UITools.run(project, "Replicating Commit", true) { progress -> progress.text = "Generating diff info..." - try { val diffInfo = generateDiffInfo(files, changes) progress.text = "Creating patch application..." val patchApp = object : PatchApp(root.toFile(), session, settings, diffInfo) { @@ -107,27 +112,22 @@ class ReplicateCommitAction : BaseAction() { loadImages = false, showMenubar = false ) - } catch (e: Throwable) { - logger.error("Error setting up patch application", e) - UITools.showErrorDialog(event.project, "Failed to set up patch application: ${e.message}", "Error") - } } - // Open browser in separate thread - Thread { + ApplicationManager.getApplication().executeOnPooledThread { Thread.sleep(500) try { - val server = AppServer.getServer(event.project) + val server = AppServer.getServer(project) val uri = server.server.uri.resolve("/#$session") logger.info("Opening browser to $uri") browse(uri) } catch (e: Throwable) { logger.error("Error opening browser", e) - UITools.showErrorDialog(event.project, "Failed to open browser: ${e.message}", "Error") + UITools.showErrorDialog(project, "Failed to open browser: ${e.message}", "Error") + } } - }.start() } catch (e: Exception) { logger.error("Error in ReplicateCommitAction", e) - UITools.showErrorDialog(event.project, "Operation failed: ${e.message}", "Error") + Messages.showErrorDialog(project, "Operation failed: ${e.message}", "Error") } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/CreateProjectorFromQueryIndexAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/CreateProjectorFromQueryIndexAction.kt index 97089ab2..e7296e1f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/CreateProjectorFromQueryIndexAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/CreateProjectorFromQueryIndexAction.kt @@ -9,9 +9,7 @@ import com.github.simiacryptus.aicoder.util.UITools import com.github.simiacryptus.aicoder.util.findRecursively import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task +import com.intellij.openapi.application.ApplicationManager import com.simiacryptus.skyenet.apps.parse.DocumentRecord import com.simiacryptus.skyenet.core.platform.Session import com.simiacryptus.skyenet.core.platform.model.User @@ -21,9 +19,13 @@ import com.simiacryptus.skyenet.webui.application.ApplicationServer import com.simiacryptus.skyenet.webui.application.ApplicationSocketManager import com.simiacryptus.skyenet.webui.session.SocketManager import org.slf4j.LoggerFactory -import kotlin.jvm.java class CreateProjectorFromQueryIndexAction : BaseAction() { + data class ProjectorConfig( + val sessionId: Session = Session.newGlobalID(), + val applicationName: String = "Projector" + ) + override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun isEnabled(event: AnActionEvent): Boolean { @@ -41,38 +43,32 @@ class CreateProjectorFromQueryIndexAction : BaseAction() { } override fun handle(e: AnActionEvent) { - val selectedFiles = UITools.getSelectedFiles(e) - val processableFiles = selectedFiles.flatMap { file -> - when { - file.isDirectory -> file.findRecursively { it.name.endsWith(".index.data") } - file.name.endsWith(".index.data") -> listOf(file) - else -> emptyList() - } - } + val processableFiles = getProcessableFiles(e) if (processableFiles.isEmpty()) { UITools.showErrorDialog(e.project, "Please select a valid query index file (.index.data).", "Invalid Selection") return } - ProgressManager.getInstance().run(object : Task.Backgroundable(e.project, "Creating Projector") { - override fun run(indicator: ProgressIndicator) { + UITools.run(e.project, "Creating Projector", true) { indicator -> try { indicator.isIndeterminate = false indicator.fraction = 0.0 + indicator.text = "Reading records..." val records = processableFiles.flatMap { DocumentRecord.readBinary(it.path) } - val sessionID = Session.newGlobalID() + val config = ProjectorConfig() + indicator.text = "Setting up projector..." - ApplicationServer.appInfoMap[sessionID] = AppInfoData( - applicationName = "Projector", - singleInput = true, - stickyInput = false, + ApplicationServer.appInfoMap[config.sessionId] = AppInfoData( + applicationName = config.applicationName, + singleInput = false, + stickyInput = true, loadImages = false, showMenubar = false ) - SessionProxyServer.Companion.chats[sessionID] = object : ApplicationServer( - applicationName = "Projector", + SessionProxyServer.chats[config.sessionId] = object : ApplicationServer( + applicationName = config.applicationName, path = "/projector", showMenubar = false, ) { @@ -82,7 +78,7 @@ class CreateProjectorFromQueryIndexAction : BaseAction() { ): SocketManager { val socketManager = super.newSession(user, session) val ui = (socketManager as ApplicationSocketManager).applicationInterface - val projector = TensorflowProjector(api, dataStorage, sessionID, ui, null) + val projector = TensorflowProjector(api, dataStorage, session, ui, null) val result = projector.writeTensorflowEmbeddingProjectorHtmlFromRecords(records) val task = ui.newTask(true) task.complete(result) @@ -91,26 +87,34 @@ class CreateProjectorFromQueryIndexAction : BaseAction() { } indicator.fraction = 1.0 + indicator.text = "Opening browser..." val server = AppServer.getServer(e.project) - Thread { + ApplicationManager.getApplication().executeOnPooledThread { Thread.sleep(500) try { - val uri = server.server.uri.resolve("/#$sessionID") + val uri = server.server.uri.resolve("/#${config.sessionId}") BaseAction.log.info("Opening browser to $uri") browse(uri) } catch (e: Throwable) { log.warn("Error opening browser", e) } - }.start() + } } catch (ex: Exception) { log.error("Error during projector creation", ex) UITools.showErrorDialog(e.project, "Error during projector creation: ${ex.message}", "Projector Creation Failed") } } - }) + } + + private fun getProcessableFiles(e: AnActionEvent) = UITools.getSelectedFiles(e).flatMap { file -> + when { + file.isDirectory -> file.findRecursively { it.name.endsWith(".index.data") } + file.name.endsWith(".index.data") -> listOf(file) + else -> emptyList() + } } companion object { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorAction.kt index 341dd714..daa6e68a 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorAction.kt @@ -25,7 +25,10 @@ import org.slf4j.LoggerFactory import java.io.File import java.nio.file.Path -class DocumentDataExtractorAction : BaseAction() { +class DocumentDataExtractorAction : BaseAction( + name = "Extract Document Data", + description = "Extracts structured data from documents using AI" +) { val path = "/pdfExtractor" private var settings = DocumentParserApp.Settings() private var modelType = ParsingModelType.Document @@ -76,40 +79,68 @@ class DocumentDataExtractorAction : BaseAction() { settings = configDialog.settings modelType = configDialog.modelType as ParsingModelType - val session = Session.newGlobalID() - DataStorage.sessionPaths[session] = selectedFile.toFile.parentFile + UITools.run(e.project, "Initializing Document Extractor", true) { progress -> + try { + progress.text = "Setting up session..." + val session = Session.newGlobalID() + DataStorage.sessionPaths[session] = selectedFile.toFile.parentFile - val smartModel = AppSettingsState.instance.smartModel.chatModel() - val parsingModel = ParsingModelType.getImpl(smartModel, 0.1, modelType) + progress.text = "Configuring AI model..." + val smartModel = AppSettingsState.instance.smartModel.chatModel() + val parsingModel = ParsingModelType.getImpl(smartModel, 0.1, modelType) - SessionProxyServer.chats[session] = object : DocumentParserApp( - applicationName = "Document Extractor", - path = this@DocumentDataExtractorAction.path, - api = this@DocumentDataExtractorAction.api, - fileInputs = processableFiles.map { it.toNioPath() }, - parsingModel = parsingModel as ParsingModel, - fastMode = settings.fastMode, - ) { - override fun initSettings(session: Session): T = this@DocumentDataExtractorAction.settings as T - override val root: File get() = selectedFile.parent.toFile - } - ApplicationServer.appInfoMap[session] = AppInfoData( - applicationName = "Code Chat", - singleInput = true, - stickyInput = false, - loadImages = false, - showMenubar = false - ) + progress.text = "Initializing document parser..." + SessionProxyServer.chats[session] = object : DocumentParserApp( + applicationName = "Document Extractor", + path = this@DocumentDataExtractorAction.path, + api = this@DocumentDataExtractorAction.api, + fileInputs = processableFiles.map { it.toNioPath() }, + parsingModel = parsingModel as ParsingModel, + fastMode = settings.fastMode, + ) { + override fun initSettings(session: Session): T = this@DocumentDataExtractorAction.settings as T + override val root: File get() = selectedFile.parent.toFile + } + val sessionId = Session.newGlobalID() + SessionProxyServer.chats[session] = DocumentParserApp( + applicationName = "Document Extractor", + path = path, + api = api, + fileInputs = processableFiles.map { it.toNioPath() }, + parsingModel = parsingModel, + fastMode = settings.fastMode + ) + ApplicationServer.appInfoMap[sessionId] = AppInfoData( + applicationName = "Document Data Extractor", + singleInput = false, + stickyInput = true, + loadImages = false, + showMenubar = false + ) + + progress.text = "Starting server..." + val server = AppServer.getServer(e.project) + launchBrowser(server, sessionId.toString()) + } catch (ex: Throwable) { + log.error("Failed to initialize document extractor", ex) + UITools.showErrorDialog( + e.project, + "Failed to initialize document extractor: ${ex.message}", + "Initialization Error" + ) + } + } + } - val server = AppServer.getServer(e.project) + private fun launchBrowser(server: AppServer, session: String) { Thread { Thread.sleep(500) try { val uri = server.server.uri.resolve("/#$session") - BaseAction.log.info("Opening browser to $uri") + log.info("Opening browser to $uri") browse(uri) } catch (e: Throwable) { - BaseAction.log.warn("Error opening browser", e) + log.warn("Error opening browser", e) } }.start() } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorConfigDialog.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorConfigDialog.kt index 2bb492a5..180c1e87 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorConfigDialog.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/DocumentDataExtractorConfigDialog.kt @@ -2,17 +2,26 @@ package com.github.simiacryptus.aicoder.actions.knowledge import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBTextField +import com.intellij.ui.layout.CCFlags +import com.intellij.ui.layout.panel import com.simiacryptus.skyenet.apps.parse.DocumentParserApp import com.simiacryptus.skyenet.apps.parse.ParsingModelType -import javax.swing.* +import javax.swing.JComboBox +import javax.swing.JComponent class DocumentDataExtractorConfigDialog( project: Project?, var settings: DocumentParserApp.Settings, var modelType: ParsingModelType<*> -) : DialogWrapper(project) { +) : DialogWrapper(project, true) { // Make dialog modal for better UX + companion object { + private const val DEFAULT_DPI = 300f + private const val DEFAULT_MAX_PAGES = 100 + private const val DEFAULT_PAGES_PER_BATCH = 10 + } private val dpiField = JBTextField(settings.dpi.toString()) private val maxPagesField = JBTextField(settings.maxPages.toString()) @@ -34,38 +43,55 @@ class DocumentDataExtractorConfigDialog( } override fun createCenterPanel(): JComponent { - val panel = JPanel() - panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS) - panel.add(createLabeledField("Parsing Model:", modelTypeComboBox)) - panel.add(createLabeledField("DPI:", dpiField)) - panel.add(createLabeledField("Max Pages:", maxPagesField)) - panel.add(createLabeledField("Output Format:", outputFormatField)) - panel.add(createLabeledField("Pages Per Batch:", pagesPerBatchField)) - panel.add(showImagesCheckbox) - panel.add(saveImageFilesCheckbox) - panel.add(saveTextFilesCheckbox) - panel.add(saveFinalJsonCheckbox) - panel.add(fastModeCheckbox) - panel.add(addLineNumbersCheckbox) - return panel + return panel { + row("Parsing Model:") { modelTypeComboBox(CCFlags.growX) } + row("DPI:") { dpiField(CCFlags.growX) } + row("Max Pages:") { maxPagesField(CCFlags.growX) } + row("Output Format:") { outputFormatField(CCFlags.growX) } + row("Pages Per Batch:") { pagesPerBatchField(CCFlags.growX) } + row { showImagesCheckbox() } + row { saveImageFilesCheckbox() } + row { saveTextFilesCheckbox() } + row { saveFinalJsonCheckbox() } + row { fastModeCheckbox() } + row { addLineNumbersCheckbox() } + } } - private fun createLabeledField(label: String, field: JComponent): JPanel { - val panel = JPanel() - panel.layout = BoxLayout(panel, BoxLayout.X_AXIS) - panel.add(JLabel(label)) - panel.add(Box.createHorizontalStrut(10)) - panel.add(field) - return panel + override fun doValidate(): ValidationInfo? { + try { + dpiField.text.toFloat().also { + if (it <= 0) return ValidationInfo("DPI must be positive", dpiField) + } + } catch (e: NumberFormatException) { + return ValidationInfo("Invalid DPI value", dpiField) + } + try { + maxPagesField.text.toInt().also { + if (it <= 0) return ValidationInfo("Max pages must be positive", maxPagesField) + } + } catch (e: NumberFormatException) { + return ValidationInfo("Invalid max pages value", maxPagesField) + } + try { + pagesPerBatchField.text.toInt().also { + if (it <= 0) return ValidationInfo("Pages per batch must be positive", pagesPerBatchField) + } + } catch (e: NumberFormatException) { + return ValidationInfo("Invalid pages per batch value", pagesPerBatchField) + } + return null } override fun doOKAction() { + if (doValidate() != null) return + settings = DocumentParserApp.Settings( - dpi = dpiField.text.toFloatOrNull() ?: settings.dpi, - maxPages = maxPagesField.text.toIntOrNull() ?: settings.maxPages, + dpi = dpiField.text.toFloatOrNull() ?: DEFAULT_DPI, + maxPages = maxPagesField.text.toIntOrNull() ?: DEFAULT_MAX_PAGES, outputFormat = outputFormatField.text, - pagesPerBatch = pagesPerBatchField.text.toIntOrNull() ?: settings.pagesPerBatch, + pagesPerBatch = pagesPerBatchField.text.toIntOrNull() ?: DEFAULT_PAGES_PER_BATCH, showImages = showImagesCheckbox.isSelected, saveImageFiles = saveImageFilesCheckbox.isSelected, saveTextFiles = saveTextFilesCheckbox.isSelected, diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/GoogleSearchAndDownloadAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/GoogleSearchAndDownloadAction.kt index 974dfad4..d9dd7077 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/GoogleSearchAndDownloadAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/knowledge/GoogleSearchAndDownloadAction.kt @@ -6,6 +6,7 @@ import com.github.simiacryptus.aicoder.config.Name import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.simiacryptus.util.JsonUtil @@ -19,6 +20,8 @@ import java.net.http.HttpResponse import javax.swing.JTextField class GoogleSearchAndDownloadAction : FileContextAction() { + private val log = Logger.getInstance(GoogleSearchAndDownloadAction::class.java) + override fun getActionUpdateThread() = ActionUpdateThread.BGT class SettingsUI { @@ -56,8 +59,15 @@ class GoogleSearchAndDownloadAction : FileContextAction { val searchQuery = config?.settings?.searchQuery ?: return emptyArray() - val searchResults = performGoogleSearch(searchQuery) - return downloadResults(searchResults, state.selectedFile) + progress.text = "Performing Google search..." + progress.isIndeterminate = true + try { + val searchResults = performGoogleSearch(searchQuery) + progress.text = "Downloading search results..." + return downloadResults(searchResults, state.selectedFile, progress) + } catch (e: Exception) { + throw RuntimeException("Failed to process search request: ${e.message}", e) + } } private fun performGoogleSearch(query: String): List> { @@ -67,17 +77,24 @@ class GoogleSearchAndDownloadAction : FileContextAction = JsonUtil.fromJson(response.body(), Map::class.java) return (searchResults["items"] as List>?) ?: emptyList() } - private fun downloadResults(results: List>, targetDir: File): Array { + private fun downloadResults(results: List>, targetDir: File, progress: ProgressIndicator): Array { val client = HttpClient.newBuilder().build() - return results.mapIndexed { index, item -> + progress.isIndeterminate = false + return results.mapIndexedNotNull { index, item -> + if (progress.isCanceled) { + throw InterruptedException("Operation cancelled by user") + } + progress.fraction = index.toDouble() / results.size val url = item["link"] as String val title = item["title"] as String + progress.text = "Downloading ${index + 1}/${results.size}: $title" val fileName = "${index + 1}_${sanitizeFileName(title)}.html" val targetFile = File(targetDir, fileName) @@ -88,13 +105,14 @@ class GoogleSearchAndDownloadAction : FileContextAction @@ -44,29 +59,42 @@ class SaveAsQueryIndexAction : BaseAction() { UITools.showErrorDialog(e.project, "No .parsed.json files found in selection.", "No Valid Files") return } - ProgressManager.getInstance().run(object : Task.Backgroundable(e.project, "Indexing Vectors", false) { + val config = getConfig(e.project) + ProgressManager.getInstance().run(object : Task.Backgroundable(e.project, "Indexing Vectors", true) { override fun run(indicator: ProgressIndicator) { - val threadPool = Executors.newFixedThreadPool(8) + val threadPool = Executors.newFixedThreadPool(config.threadCount) try { indicator.isIndeterminate = false indicator.fraction = 0.0 + indicator.text = "Initializing vector indexing..." + saveAsBinary( openAIClient = IdeaOpenAIClient.instance, pool = threadPool, progressState = ProgressState().apply { onUpdate += { indicator.fraction = it.progress / it.max + indicator.text = "Processing files (${it.progress}/${it.max})" + if (indicator.isCanceled) { + throw InterruptedException("Operation cancelled by user") + } } }, inputPaths = jsonFiles.map { it.path }.toTypedArray() ) + indicator.fraction = 1.0 + indicator.text = "Vector indexing complete" log.info("Conversion to Data complete") + UITools.showInfoMessage(e.project, "Vector indexing completed successfully", "Success") + } catch (ex: InterruptedException) { + log.info("Vector indexing cancelled by user") + UITools.showInfoMessage(e.project, "Vector indexing cancelled", "Cancelled") } catch (ex: Exception) { log.error("Error during binary conversion", ex) UITools.showErrorDialog(e.project, "Error during conversion: ${ex.message}", "Conversion Failed") } finally { - threadPool.shutdown() + threadPool.shutdownNow() } } }) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/AppendTextWithChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/AppendTextWithChatAction.kt index 5e2d1b36..da731fc4 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/AppendTextWithChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/AppendTextWithChatAction.kt @@ -2,6 +2,7 @@ package com.github.simiacryptus.aicoder.actions.legacy import com.github.simiacryptus.aicoder.actions.SelectionAction import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.Project @@ -9,6 +10,13 @@ import com.simiacryptus.jopenai.models.ApiModel.* import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.util.ClientUtil.toContentList +/** + * Action that appends AI-generated text to the current selection. + * Uses ChatGPT to generate contextually relevant continuations of the selected text. + * + * @see SelectionAction + */ + class AppendTextWithChatAction : SelectionAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -19,20 +27,27 @@ class AppendTextWithChatAction : SelectionAction() { } override fun processSelection(state: SelectionState, config: String?): String { - val settings = AppSettingsState.instance - val request = ChatRequest( - model = settings.smartModel, - temperature = settings.temperature - ).copy( - temperature = settings.temperature, - messages = listOf( - ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), - ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) - ), - ) - val chatResponse = api.chat(request, settings.smartModel.chatModel()) - val b4 = state.selectedText ?: "" - val str = chatResponse.choices[0].message?.content ?: "" - return b4 + if (str.startsWith(b4)) str.substring(b4.length) else str + try { + val settings = AppSettingsState.instance + val request = ChatRequest( + model = settings.smartModel, + temperature = settings.temperature + ).copy( + temperature = settings.temperature, + messages = listOf( + ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), + ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) + ), + ) + val chatResponse = api.chat(request, settings.smartModel.chatModel()) + val originalText = state.selectedText ?: "" + val generatedText = chatResponse.choices[0].message?.content ?: "" + // Remove duplicate text if AI response includes the original text + return originalText + if (generatedText.startsWith(originalText)) + generatedText.substring(originalText.length) else generatedText + } catch (e: Exception) { + UITools.error(log, "Failed to generate text continuation", e) + return state.selectedText ?: "" + } } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/CommentsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/CommentsAction.kt index 03541b52..26f46c2d 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/CommentsAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/CommentsAction.kt @@ -6,14 +6,20 @@ import com.github.simiacryptus.aicoder.util.ComputerLanguage import com.github.simiacryptus.aicoder.util.LanguageUtils import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy class CommentsAction : SelectionAction() { + private val log = Logger.getInstance(CommentsAction::class.java) + override fun getActionUpdateThread() = ActionUpdateThread.BGT - override fun isEnabled(event: AnActionEvent) = AppSettingsState.instance.enableLegacyActions + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + return AppSettingsState.instance.enableLegacyActions + } override fun getConfig(project: Project?): String { return "" @@ -24,18 +30,25 @@ class CommentsAction : SelectionAction() { } override fun processSelection(state: SelectionState, config: String?): String { - return ChatProxy( - clazz = CommentsAction_VirtualAPI::class.java, - api = api, - temperature = AppSettingsState.instance.temperature, - model = AppSettingsState.instance.smartModel.chatModel(), - deserializerRetries = 5 - ).create().editCode( - state.selectedText ?: "", - "Add comments to each line explaining the code", - state.language.toString(), - AppSettingsState.instance.humanLanguage - ).code ?: "" + try { + val selectedText = state.selectedText ?: return "" + val language = state.language?.toString() ?: return selectedText + return ChatProxy( + clazz = CommentsAction_VirtualAPI::class.java, + api = api, + temperature = AppSettingsState.instance.temperature, + model = AppSettingsState.instance.smartModel.chatModel(), + deserializerRetries = 5 + ).create().editCode( + selectedText, + "Add comments to each line explaining the code", + language, + AppSettingsState.instance.humanLanguage + ).code ?: selectedText + } catch (e: Exception) { + log.error("Failed to process comments", e) + throw e + } } interface CommentsAction_VirtualAPI { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/DocAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/DocAction.kt index d96af2fb..da501995 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/DocAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/DocAction.kt @@ -11,8 +11,19 @@ import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy +/** + * Action that generates documentation for selected code blocks. + * Supports multiple programming languages and documentation styles. + */ + class DocAction : SelectionAction() { + companion object { + private const val DEFAULT_DESERIALIZER_RETRIES = 5 + } + + private val log = com.intellij.openapi.diagnostic.Logger.getInstance(DocAction::class.java) override fun getActionUpdateThread() = ActionUpdateThread.BGT + fun getDisplayName() = "Generate Documentation" override fun isEnabled(event: AnActionEvent) = AppSettingsState.instance.enableLegacyActions @@ -36,7 +47,7 @@ class DocAction : SelectionAction() { api = api, model = AppSettingsState.instance.smartModel.chatModel(), temperature = AppSettingsState.instance.temperature, - deserializerRetries = 5 + deserializerRetries = DEFAULT_DESERIALIZER_RETRIES ) chatProxy.addExample( DocAction_VirtualAPI.DocAction_ConvertedText().apply { @@ -67,15 +78,24 @@ class DocAction : SelectionAction() { } override fun processSelection(state: SelectionState, config: String?): String { - val code = state.selectedText - val indentedInput = IndentedText.fromString(code.toString()) - val docString = proxy.processCode( - indentedInput.textBlock.toString(), - "Write detailed " + (state.language?.docStyle ?: "documentation") + " prefix for code block", - state.language?.name ?: "", - AppSettingsState.instance.humanLanguage - ).text ?: "" - return docString + code + try { + val code = state.selectedText ?: return "" + val indentedInput = IndentedText.fromString(code) + val docString = proxy.processCode( + indentedInput.textBlock.toString(), + "Write detailed " + (state.language?.docStyle ?: "documentation") + " prefix for code block", + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ).text ?: "" + return docString + code + } catch (e: Exception) { + log.error("Failed to generate documentation", e) + throw RuntimeException( + "Failed to generate documentation: ${e.message}", + e + ) + } + } override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ImplementStubAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ImplementStubAction.kt index 99c1f8dc..cdd9deba 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ImplementStubAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ImplementStubAction.kt @@ -3,16 +3,24 @@ package com.github.simiacryptus.aicoder.actions.legacy import com.github.simiacryptus.aicoder.actions.SelectionAction import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.UITools import com.github.simiacryptus.aicoder.util.psi.PsiUtil import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy import com.simiacryptus.util.StringUtil import java.util.* +/** + * Action that implements stub methods/classes using AI code generation. + * Extends SelectionAction to handle code selection and language detection. + */ + class ImplementStubAction : SelectionAction() { + private val log = Logger.getInstance(ImplementStubAction::class.java) override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun isEnabled(event: AnActionEvent) = AppSettingsState.instance.enableLegacyActions @@ -57,16 +65,27 @@ class ImplementStubAction : SelectionAction() { } override fun processSelection(state: SelectionState, config: String?): String { - val code = state.selectedText ?: "" - val settings = AppSettingsState.instance - val outputHumanLanguage = settings.humanLanguage - val computerLanguage = state.language + try { + val code = state.selectedText ?: "" + val settings = AppSettingsState.instance + val outputHumanLanguage = settings.humanLanguage + val computerLanguage = state.language ?: return code + if (!isLanguageSupported(computerLanguage)) { + UITools.showWarning(null, "Language ${computerLanguage.name} is not supported") + return code + } + return processCode(code, state, computerLanguage, outputHumanLanguage) + } catch (e: Exception) { + log.error("Error implementing stub", e) + UITools.showError(null, "Failed to implement stub: ${e.message}") + return state.selectedText ?: "" + } + } + + private fun processCode(code: String, state: SelectionState, computerLanguage: ComputerLanguage, outputHumanLanguage: String): String { val codeContext = state.contextRanges.filter { - PsiUtil.matchesType( - it.name, - PsiUtil.ELEMENTS_CODE - ) + PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_CODE) } var smallestIntersectingMethod = "" if (codeContext.isNotEmpty()) smallestIntersectingMethod = @@ -79,7 +98,7 @@ class ImplementStubAction : SelectionAction() { return getProxy().editCode( declaration, "Implement Stub", - computerLanguage?.name?.lowercase(Locale.ROOT) ?: "", + computerLanguage.name.lowercase(Locale.ROOT), outputHumanLanguage ).code ?: "" } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/InsertImplementationAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/InsertImplementationAction.kt index 8ce6f94f..28da302c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/InsertImplementationAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/InsertImplementationAction.kt @@ -44,7 +44,17 @@ class InsertImplementationAction : SelectionAction() { } override fun getConfig(project: Project?): String { - return "" + try { + // Validate settings before proceeding + if (AppSettingsState.instance.smartModel == null) { + UITools.showErrorDialog(project, "AI model not configured", "Configuration Error") + return "" + } + return "" + } catch (e: Exception) { + UITools.error(log, "Failed to get configuration", e) + return "" + } } override fun defaultSelection(editorState: EditorState, offset: Int): Pair { @@ -68,45 +78,56 @@ class InsertImplementationAction : SelectionAction() { } override fun processSelection(state: SelectionState, config: String?): String { - val humanLanguage = AppSettingsState.instance.humanLanguage - val computerLanguage = state.language - val psiClassContextActionParams = getPsiClassContextActionParams(state) - val selectedText = state.selectedText ?: "" + val humanLanguage: String = AppSettingsState.instance.humanLanguage + val computerLanguage: ComputerLanguage? = state.language + try { + val psiClassContextActionParams = getPsiClassContextActionParams(state) + val selectedText = state.selectedText ?: "" - val comment = psiClassContextActionParams.largestIntersectingComment - var instruct = comment?.subString(state.entireDocument ?: "")?.trim() ?: selectedText - if (selectedText.split(" ").dropWhile { it.isEmpty() }.size > 4) { - instruct = selectedText.trim() - } - val fromString: TextBlock? = computerLanguage?.getCommentModel(instruct)?.fromString(instruct) - val specification = fromString?.rawString()?.map { it.toString().trim() } - ?.filter { it.isNotEmpty() }?.reduce { a, b -> "$a $b" } ?: return selectedText - val code = if (state.psiFile != null) { - UITools.run(state.project, "Insert Implementation", true, true) { - val psiClassContext = runReadAction { - PsiClassContext.getContext( - state.psiFile, - psiClassContextActionParams.selectionStart, - psiClassContextActionParams.selectionEnd, - computerLanguage - ).toString() + val comment = psiClassContextActionParams.largestIntersectingComment + var instruct = comment?.subString(state.entireDocument ?: "")?.trim() ?: selectedText + if (selectedText.split(" ").dropWhile { it.isEmpty() }.size > 4) { + instruct = selectedText.trim() + } + val fromString: TextBlock? = computerLanguage?.getCommentModel(instruct)?.fromString(instruct) + val specification = fromString?.rawString()?.map { it.toString().trim() } + ?.filter { it.isNotEmpty() }?.reduce { a, b -> "$a $b" } ?: return selectedText + val code = if (state.psiFile != null) { + UITools.run(state.project, "Insert Implementation", true, true) { progress -> + progress.isIndeterminate = false + progress.text = "Analyzing context..." + progress.fraction = 0.2 + val psiClassContext = runReadAction { + PsiClassContext.getContext( + state.psiFile, + psiClassContextActionParams.selectionStart, + psiClassContextActionParams.selectionEnd, + computerLanguage + ).toString() + } + progress.text = "Generating implementation..." + progress.fraction = 0.6 + getProxy().implementCode( + specification = specification, + prefix = psiClassContext, + computerLanguage = computerLanguage.name, + humanLanguage = humanLanguage + ).code ?: throw IllegalStateException("No code generated") } - getProxy().implementCode( - specification, - psiClassContext, - computerLanguage.name, - humanLanguage - ).code + } else { + getProxy().implementCode(specification, "", computerLanguage.name, humanLanguage).code } - } else { getProxy().implementCode( specification, "", computerLanguage.name, humanLanguage ).code + return if (code != null) "$selectedText\n${state.indent}$code" else selectedText + } catch (e: Exception) { + UITools.error(log, "Failed to process selection", e) + return state.selectedText ?: "" } - return if (code != null) "$selectedText\n${state.indent}$code" else selectedText } private fun getPsiClassContextActionParams(state: SelectionState): PsiClassContextActionParams { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/RenameVariablesAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/RenameVariablesAction.kt index 6ee6a838..3fb90a00 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/RenameVariablesAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/RenameVariablesAction.kt @@ -7,11 +7,19 @@ import com.github.simiacryptus.aicoder.util.LanguageUtils import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy -open class RenameVariablesAction : SelectionAction() { +/** + * Action to suggest and apply variable name improvements in code. + * Supports multiple programming languages and uses AI to generate naming suggestions. + */ + +class RenameVariablesAction : SelectionAction() { + private val log = Logger.getInstance(RenameVariablesAction::class.java) + override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun isEnabled(event: AnActionEvent) = AppSettingsState.instance.enableLegacyActions @@ -48,38 +56,55 @@ open class RenameVariablesAction : SelectionAction() { return "" } + @Throws(Exception::class) + override fun processSelection(event: AnActionEvent?, state: SelectionState, config: String?): String { - val renameSuggestions = UITools.run(event?.project, templateText, true, true) { - proxy - .suggestRenames( - state.selectedText ?: "", - state.language?.name ?: "", - AppSettingsState.instance.humanLanguage - ) - .suggestions - .filter { it.originalName != null && it.suggestedName != null } - .associate { it.originalName!! to it.suggestedName!! } - } - val selectedSuggestions = choose(renameSuggestions) - return UITools.run(event?.project, templateText, true, true) { - var selectedText = state.selectedText - val filter = renameSuggestions.filter { it.key in selectedSuggestions } - filter.forEach { (key, value) -> - selectedText = selectedText?.replace(key, value) + try { + val renameSuggestions = UITools.run(event?.project, "Analyzing Code", true, true) { progress -> + progress.text = "Generating rename suggestions..." + proxy + .suggestRenames( + state.selectedText ?: "", + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ) + .suggestions + .filter { it.originalName != null && it.suggestedName != null } + .associate { it.originalName!! to it.suggestedName!! } + } + if (renameSuggestions.isEmpty()) { + UITools.showInfoMessage(event?.project, "No rename suggestions found", "No Changes") + return state.selectedText ?: "" } - selectedText ?: "" + val selectedSuggestions = Companion.choose(renameSuggestions) + return UITools.run(event?.project, "Applying Changes", true, true) { progress -> + progress.text = "Applying selected renames..." + var selectedText = state.selectedText + val filter = renameSuggestions.filter { it.key in selectedSuggestions } + filter.forEach { (key, value) -> + selectedText = selectedText?.replace(key, value) + } + selectedText ?: "" + } + } catch (e: Exception) { + log.error("Error during rename operation", e) + UITools.showErrorDialog(event?.project, "Failed to process rename operation: ${e.message}", "Error") + throw e } } - open fun choose(renameSuggestions: Map): Set { - return UITools.showCheckboxDialog( - "Select which items to rename", - renameSuggestions.keys.toTypedArray(), - renameSuggestions.map { (key, value) -> "$key -> $value" }.toTypedArray() - ).toSet() - } override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { return LanguageUtils.isLanguageSupported(computerLanguage) } + + companion object { + fun choose(renameSuggestions: Map): Set { + return UITools.showCheckboxDialog( + "Select which items to rename", + renameSuggestions.keys.toTypedArray(), + renameSuggestions.map { (key, value) -> "$key -> $value" }.toTypedArray() + ).toSet() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ReplaceWithSuggestionsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ReplaceWithSuggestionsAction.kt index f182ec9c..4cf25fa2 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ReplaceWithSuggestionsAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/legacy/ReplaceWithSuggestionsAction.kt @@ -5,6 +5,7 @@ import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy @@ -14,6 +15,9 @@ import kotlin.math.ln import kotlin.math.pow open class ReplaceWithSuggestionsAction : SelectionAction() { + private val log = Logger.getInstance(ReplaceWithSuggestionsAction::class.java) + private val templateText = "Generating suggestions..." + override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun isEnabled(event: AnActionEvent) = AppSettingsState.instance.enableLegacyActions @@ -38,25 +42,43 @@ open class ReplaceWithSuggestionsAction : SelectionAction() { } override fun getConfig(project: Project?): String { + // Could be enhanced to get user preferences for suggestion generation return "" } override fun processSelection(event: AnActionEvent?, state: SelectionState, config: String?): String { - val choices = UITools.run(event?.project, templateText, true, true) { - val selectedText = state.selectedText - val idealLength = 2.0.pow(2 + ceil(ln(selectedText?.length?.toDouble() ?: 1.0))).toInt() - val selectionStart = state.selectionOffset - val allBefore = state.entireDocument?.substring(0, selectionStart) ?: "" - val selectionEnd = state.selectionOffset + (state.selectionLength ?: 0) - val allAfter = state.entireDocument?.substring(selectionEnd, state.entireDocument.length) ?: "" - val before = StringUtil.getSuffixForContext(allBefore, idealLength).toString().replace('\n', ' ') - val after = StringUtil.getPrefixForContext(allAfter, idealLength).toString().replace('\n', ' ') - proxy.suggestText( - "$before _____ $after", - listOf(selectedText.toString()) - ).choices + try { + val choices: List = UITools.run(event?.project, templateText, true, true) { progress -> + progress.isIndeterminate = false + progress.text = "Analyzing context..." + progress.fraction = 0.2 + val selectedText = state.selectedText ?: return@run emptyList() + val idealLength = 2.0.pow(2 + ceil(ln(selectedText.length.toDouble()))).toInt() + progress.text = "Preparing context..." + progress.fraction = 0.4 + val selectionStart = state.selectionOffset + val allBefore = state.entireDocument?.substring(0, selectionStart) ?: "" + val selectionEnd = state.selectionOffset + (state.selectionLength ?: 0) + val allAfter = state.entireDocument?.substring(selectionEnd, state.entireDocument.length) ?: "" + val before = StringUtil.getSuffixForContext(allBefore, idealLength).toString().replace('\n', ' ') + val after = StringUtil.getPrefixForContext(allAfter, idealLength).toString().replace('\n', ' ') + progress.text = "Generating suggestions..." + progress.fraction = 0.6 + proxy.suggestText( + "$before _____ $after", + listOf(selectedText) + ).choices ?: emptyList() + } + return choose(choices) + } catch (e: Exception) { + log.error("Failed to generate suggestions", e) + UITools.showErrorDialog( + event?.project, + "Failed to generate suggestions: ${e.message}", + "Error" + ) + return state.selectedText ?: "" } - return choose(choices ?: listOf()) } open fun choose(choices: List): String { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt index b40c22e5..2e711b56 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt @@ -11,6 +11,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy +import org.slf4j.LoggerFactory class MarkdownImplementActionGroup : ActionGroup() { override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -26,10 +27,15 @@ class MarkdownImplementActionGroup : ActionGroup() { } companion object { + private val log = LoggerFactory.getLogger(MarkdownImplementActionGroup::class.java) fun isEnabled(e: AnActionEvent): Boolean { - val computerLanguage = ComputerLanguage.getComputerLanguage(e) ?: return false - if (ComputerLanguage.Markdown != computerLanguage) return false - return UITools.hasSelection(e) + return try { + val computerLanguage = ComputerLanguage.getComputerLanguage(e) ?: return false + ComputerLanguage.Markdown == computerLanguage && UITools.hasSelection(e) + } catch (ex: Exception) { + log.error("Error checking action enablement", ex) + false + } } } @@ -72,15 +78,26 @@ class MarkdownImplementActionGroup : ActionGroup() { } override fun processSelection(state: SelectionState, config: String?): String { - val code = getProxy().implement(state.selectedText ?: "", "autodetect", language).code ?: "" - return """ - | - | - |```$language - |${code.let { /*escapeHtml4*/(it)/*.indent(" ")*/ }} - |``` - | - |""".trimMargin() + return try { + UITools.run(state.project, "Converting to $language", true) { progress -> + progress.text = "Generating $language code..." + progress.isIndeterminate = true + val code = getProxy().implement(state.selectedText ?: "", "autodetect", language).code + ?: throw IllegalStateException("No code generated") + """ + | + | + |```$language + |${code.trim()} + |``` + | + |""".trimMargin() + } + } catch (e: Exception) { + log.error("Error processing selection", e) + UITools.showErrorDialog(state.project, "Failed to convert code: ${e.message}", "Conversion Error") + state.selectedText ?: "" + } } } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt index 54f16208..c4458269 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt @@ -1,8 +1,7 @@ -package com.github.simiacryptus.aicoder.actions.markdown +package com.github.simiacryptus.aicoder.actions.markdown import com.github.simiacryptus.aicoder.actions.BaseAction import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage import com.github.simiacryptus.aicoder.util.UITools import com.github.simiacryptus.aicoder.util.UITools.getIndent import com.github.simiacryptus.aicoder.util.UITools.insertString @@ -12,12 +11,48 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.jopenai.proxy.ChatProxy import com.simiacryptus.util.StringUtil +import java.awt.Component +import javax.swing.JOptionPane +/** + * Action that extends markdown lists by generating additional items using AI. + * Supports bullet lists and checkbox lists. + */ class MarkdownListAction : BaseAction() { - override fun getActionUpdateThread() = ActionUpdateThread.BGT + private val log = Logger.getInstance(MarkdownListAction::class.java) + private lateinit var progress: ProgressIndicator + + data class ListConfig( + val itemCount: Int = 0, + val temperature: Double = AppSettingsState.instance.temperature + ) + + /** + * Gets configuration for list generation + */ + private fun getConfig(project: Project?): ListConfig? { + return try { + ListConfig( + itemCount = UITools.showInputDialog( + project as? Component, + "How many new items to generate?", + "Generate List Items", + JOptionPane.QUESTION_MESSAGE + )?.let { Integer.parseInt(it.toString()) } ?: return null + ) + } catch (e: Exception) { + log.warn("Failed to get configuration", e) + null + } + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT interface ListAPI { fun newListItems( @@ -37,6 +72,7 @@ class MarkdownListAction : BaseAction() { api = api, model = AppSettingsState.instance.smartModel.chatModel(), deserializerRetries = 5, + temperature = AppSettingsState.instance.temperature ) chatProxy.addExample( returnValue = ListAPI.Items( @@ -52,7 +88,11 @@ class MarkdownListAction : BaseAction() { } override fun handle(e: AnActionEvent) { - val caret = e.getData(CommonDataKeys.CARET) ?: return + try { + val project = e.project ?: return + val config = getConfig(project) ?: return + + val caret = e.getData(CommonDataKeys.CARET) ?: return val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: return val list = getSmallestIntersecting(psiFile, caret.selectionStart, caret.selectionEnd, "MarkdownListImpl") ?: return @@ -63,6 +103,8 @@ class MarkdownListAction : BaseAction() { if (all.isEmpty()) it.text else all[0].text }.toList(), 10, false ) + progress.fraction = 0.4 + progress.text = "Generating new items..." val indent = getIndent(caret) val endOffset = list.textRange.endOffset val bulletTypes = listOf("- [ ] ", "- ", "* ") @@ -75,13 +117,18 @@ class MarkdownListAction : BaseAction() { UITools.redoableTask(e) { var newItems: List? = null + progress.isIndeterminate = false + progress.fraction = 0.2 + progress.text = "Analyzing existing items..." UITools.run( e.project, "Generating New Items", true ) { newItems = proxy.newListItems( rawItems, - (items.size * 2) + config.itemCount ).items + progress.fraction = 0.8 + progress.text = "Formatting results..." } var newList = "" ApplicationManager.getApplication().runReadAction { @@ -95,17 +142,21 @@ class MarkdownListAction : BaseAction() { insertString(document, endOffset, "\n" + newList) } } - } - override fun isEnabled(event: AnActionEvent): Boolean { - val computerLanguage = ComputerLanguage.getComputerLanguage(event) ?: return false - if (ComputerLanguage.Markdown != computerLanguage) return false - val caret = event.getData(CommonDataKeys.CARET) ?: return false - val psiFile = event.getData(CommonDataKeys.PSI_FILE) ?: return false - getSmallestIntersecting(psiFile, caret.selectionStart, caret.selectionEnd, "MarkdownListImpl") ?: return false - return true + } catch (ex: Exception) { + log.error("Failed to generate list items", ex) + UITools.showErrorDialog( + e.project, + "Failed to generate list items: ${ex.message}", + "Error" + ) + } } -} - + override fun isEnabled(e: AnActionEvent): Boolean { + val enabled = super.isEnabled(e) + e.presentation.isEnabledAndVisible = enabled + return enabled + } +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/AutoPlanChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/AutoPlanChatAction.kt index 13291211..90512066 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/AutoPlanChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/AutoPlanChatAction.kt @@ -10,6 +10,7 @@ import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.vfs.VirtualFile import com.simiacryptus.diff.FileValidationUtils import com.simiacryptus.jopenai.models.chatModel @@ -21,11 +22,15 @@ import com.simiacryptus.skyenet.core.platform.file.DataStorage import com.simiacryptus.skyenet.core.util.getModuleRootForFile import com.simiacryptus.skyenet.webui.application.AppInfoData import com.simiacryptus.skyenet.webui.application.ApplicationServer -import org.slf4j.LoggerFactory import java.io.File import java.nio.file.Path class AutoPlanChatAction : BaseAction() { + // Maximum file size to process (512KB) + private companion object { + private const val MAX_FILE_SIZE = 512 * 1024 + } + override fun getActionUpdateThread() = ActionUpdateThread.BGT @@ -37,7 +42,7 @@ class AutoPlanChatAction : BaseAction() { command = listOf( if (System.getProperty("os.name").lowercase().contains("win")) "powershell" else "bash" ), - temperature = AppSettingsState.instance.temperature, + temperature = AppSettingsState.instance.temperature.coerceIn(0.0, 1.0), workingDir = UITools.getRoot(e), env = mapOf(), githubToken = AppSettingsState.instance.githubToken, @@ -46,71 +51,101 @@ class AutoPlanChatAction : BaseAction() { ) ) if (dialog.showAndGet()) { - // Settings are applied only if the user clicks OK - val session = Session.newGlobalID() - val folder = UITools.getSelectedFolder(e) - val root = folder?.toFile ?: getModuleRootForFile( - UITools.getSelectedFile(e)?.parent?.toFile ?: throw RuntimeException("") - ) - DataStorage.sessionPaths[session] = root - SessionProxyServer.Companion.chats[session] = object : AutoPlanChatApp( - planSettings = dialog.settings.copy( - env = mapOf(), - workingDir = root.absolutePath, - language = if (isWindows) "powershell" else "bash", - command = listOf( - if (System.getProperty("os.name").lowercase().contains("win")) "powershell" else "bash" - ), - parsingModel = AppSettingsState.instance.fastModel.chatModel(), + try { + UITools.run(e.project, "Initializing Auto Plan Chat", true) { progress -> + initializeChat(e, dialog, progress) + } + } catch (ex: Exception) { + log.error("Failed to initialize chat", ex) + UITools.showError(e.project, "Failed to initialize chat: ${ex.message}") + } + } + } + + private fun initializeChat(e: AnActionEvent, dialog: PlanAheadConfigDialog, progress: ProgressIndicator) { + progress.text = "Setting up session..." + val session = Session.newGlobalID() + val root = getProjectRoot(e) ?: throw RuntimeException("Could not determine project root") + progress.text = "Processing files..." + setupChatSession(session, root, e, dialog) + progress.text = "Starting server..." + val server = AppServer.getServer(e.project) + openBrowser(server, session.toString()) + } + + private fun getProjectRoot(e: AnActionEvent): File? { + val folder = UITools.getSelectedFolder(e) + return folder?.toFile ?: UITools.getSelectedFile(e)?.parent?.toFile?.let { file -> + getModuleRootForFile(file) + } + } + + private fun setupChatSession(session: Session, root: File, e: AnActionEvent, dialog: PlanAheadConfigDialog) { + DataStorage.sessionPaths[session] = root + SessionProxyServer.chats[session] = createChatApp(root, e, dialog) + ApplicationServer.appInfoMap[session] = AppInfoData( + applicationName = "Auto Plan Chat", + singleInput = false, + stickyInput = true, + loadImages = false, + showMenubar = false + ) + } + + private fun createChatApp(root: File, e: AnActionEvent, dialog: PlanAheadConfigDialog) = + object : AutoPlanChatApp( + planSettings = dialog.settings.copy( + env = mapOf(), + workingDir = root.absolutePath, + language = if (isWindows) "powershell" else "bash", + command = listOf( + if (System.getProperty("os.name").lowercase().contains("win")) "powershell" else "bash" ), - model = AppSettingsState.instance.smartModel.chatModel(), parsingModel = AppSettingsState.instance.fastModel.chatModel(), - showMenubar = false, - api = api, - api2 = api2, - ) { - fun codeFiles() = (UITools.getSelectedFiles(e).toTypedArray()?.toList()?.flatMap { - FileValidationUtils.expandFileList(it.toFile).toList() - }?.map { it.toPath() }?.toSet()?.toMutableSet() ?: mutableSetOf()) - .filter { it.toFile().exists() } - .filter { it.toFile().length() < 1024 * 1024 / 2 } - .map { root.toPath().relativize(it) ?: it }.toSet() + ), + model = AppSettingsState.instance.smartModel.chatModel(), + parsingModel = AppSettingsState.instance.fastModel.chatModel(), + showMenubar = false, + api = api, + api2 = api2, + ) { + private fun codeFiles() = (UITools.getSelectedFiles(e).toTypedArray().toList().flatMap { + FileValidationUtils.expandFileList(it.toFile).toList() + }.map { it.toPath() }.toSet()?.toMutableSet() ?: mutableSetOf()) + .filter { it.toFile().exists() } + .filter { it.toFile().length() < MAX_FILE_SIZE } + .map { root.toPath().relativize(it) ?: it }.toSet() - fun codeSummary() = codeFiles() - .joinToString("\n\n") { path -> - """ - |# ${path} - |$tripleTilde${path.toString().split('.').lastOrNull()} - |${root.resolve(path.toFile()).readText(Charsets.UTF_8)} - |$tripleTilde - """.trimMargin() - } + private fun codeSummary() = codeFiles() + .joinToString("\n\n") { path -> + """ + |# ${path} + |$tripleTilde${path.toString().split('.').lastOrNull()} + |${root.resolve(path.toFile()).readText(Charsets.UTF_8)} + |$tripleTilde + """.trimMargin() + } - fun projectSummary() = codeFiles() - .asSequence().distinct().sorted() - .joinToString("\n") { path -> - "* ${path} - ${root.resolve(path.toFile()).length()} bytes" - } + private fun projectSummary() = codeFiles() + .asSequence().distinct().sorted() + .joinToString("\n") { path -> + "* ${path} - ${root.resolve(path.toFile()).length()} bytes" + } - override fun contextData(): List = listOf( - if (codeFiles().size < 4) { + override fun contextData(): List = + try { + listOf( + if (codeFiles().size < 4) { "Files:\n" + codeSummary() } else { "Files:\n" + projectSummary() - }, - ) - } - ApplicationServer.appInfoMap[session] = AppInfoData( - applicationName = "Auto Plan Chat", - singleInput = false, - stickyInput = true, - loadImages = false, - showMenubar = false - ) - val server = AppServer.getServer(e.project) - openBrowser(server, session.toString()) + } + ) + } catch (e: Exception) { + log.error("Error generating context data", e) + emptyList() + } } - } private fun openBrowser(server: AppServer, session: String) { Thread { @@ -125,7 +160,4 @@ class AutoPlanChatAction : BaseAction() { }.start() } - companion object { - private val log = LoggerFactory.getLogger(AutoPlanChatAction::class.java) - } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/PlanChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/PlanChatAction.kt index 974a1ae8..4d7a5638 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/PlanChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/plan/PlanChatAction.kt @@ -9,6 +9,8 @@ import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction import com.simiacryptus.jopenai.models.chatModel import com.simiacryptus.skyenet.apps.general.PlanChatApp import com.simiacryptus.skyenet.apps.plan.PlanSettings @@ -21,11 +23,33 @@ import com.simiacryptus.skyenet.webui.application.ApplicationServer import org.slf4j.LoggerFactory import kotlin.collections.set +/** + * Action that opens a Plan Chat interface for executing and planning commands. + * Supports both Windows (PowerShell) and Unix (Bash) environments. + */ + class PlanChatAction : BaseAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT + override fun isEnabled(e: AnActionEvent): Boolean { + if (!super.isEnabled(e)) return false + return UITools.getSelectedFolder(e) != null || UITools.getSelectedFile(e) != null + } + override fun handle(e: AnActionEvent) { + try { + UITools.run(e.project, "Initializing Plan Chat", true) { progress -> + progress.isIndeterminate = true + progress.text = "Setting up chat environment..." + initializeAndOpenChat(e) + } + } catch (ex: Throwable) { + UITools.error(log, "Failed to initialize Plan Chat", ex) + } + } + + private fun initializeAndOpenChat(e: AnActionEvent) { val dialog = PlanAheadConfigDialog( e.project, PlanSettings( defaultModel = AppSettingsState.instance.smartModel.chatModel(), @@ -42,7 +66,12 @@ class PlanChatAction : BaseAction() { ) ) if (dialog.showAndGet()) { - // Settings are applied only if the user clicks OK + setupChatSession(e, dialog.settings) + } + } + + private fun setupChatSession(e: AnActionEvent, settings: PlanSettings) { + WriteCommandAction.runWriteCommandAction(e.project) { val session = Session.newGlobalID() val folder = UITools.getSelectedFolder(e) val root = folder?.toFile ?: getModuleRootForFile( @@ -50,7 +79,7 @@ class PlanChatAction : BaseAction() { ) DataStorage.sessionPaths[session] = root SessionProxyServer.Companion.chats[session] = PlanChatApp( - planSettings = dialog.settings.copy( + planSettings = settings.copy( env = mapOf(), workingDir = root.absolutePath, language = if (isWindows) "powershell" else "bash", @@ -78,16 +107,17 @@ class PlanChatAction : BaseAction() { } private fun openBrowser(server: AppServer, session: String) { - Thread { + ApplicationManager.getApplication().invokeLater { Thread.sleep(500) try { val uri = server.server.uri.resolve("/#$session") log.info("Opening browser to $uri") browse(uri) } catch (e: Throwable) { + log.error("Failed to open browser", e) LoggerFactory.getLogger(PlanChatAction::class.java).warn("Error opening browser", e) } - }.start() + } } companion object { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/test/TestResultAutofixAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/test/TestResultAutofixAction.kt index b1e10b9e..7176c0e7 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/test/TestResultAutofixAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/test/TestResultAutofixAction.kt @@ -6,13 +6,12 @@ import com.github.simiacryptus.aicoder.actions.generic.SessionProxyServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.github.simiacryptus.aicoder.util.IdeaChatClient +import com.github.simiacryptus.aicoder.util.UITools import com.intellij.execution.testframework.AbstractTestProxy import com.intellij.execution.testframework.sm.runner.SMTestProxy import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.vfs.VirtualFile -import com.simiacryptus.diff.FileValidationUtils import com.simiacryptus.diff.FileValidationUtils.Companion.isGitignore import com.simiacryptus.diff.addApplyFileDiffLinks import com.simiacryptus.jopenai.models.chatModel @@ -31,13 +30,13 @@ import com.simiacryptus.skyenet.webui.session.SessionTask import com.simiacryptus.skyenet.webui.session.SocketManager import com.simiacryptus.util.JsonUtil import org.jetbrains.annotations.NotNull +import org.slf4j.LoggerFactory import java.io.File import java.nio.file.Path -import javax.swing.JOptionPane class TestResultAutofixAction : BaseAction() { companion object { - private val log = Logger.getInstance(TestResultAutofixAction::class.java) + private val log = LoggerFactory.getLogger(TestResultAutofixAction::class.java) val tripleTilde = "`" + "``" // This is a workaround for the markdown parser when editing this file fun getFiles( @@ -118,16 +117,17 @@ class TestResultAutofixAction : BaseAction() { val dataContext = e.dataContext val virtualFile = PlatformDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext)?.firstOrNull() val root = Companion.findGitRoot(virtualFile) - Thread { + UITools.run(e.project, "Analyzing Test Result", true) { progress -> + progress.isIndeterminate = true + progress.text = "Analyzing test failure..." try { val testInfo = getTestInfo(testProxy) val projectStructure = getProjectStructure(root) openAutofixWithTestResult(e, testInfo, projectStructure) } catch (ex: Throwable) { - log.error("Error analyzing test result", ex) - JOptionPane.showMessageDialog(null, ex.message, "Error", JOptionPane.ERROR_MESSAGE) + UITools.error(log, "Error analyzing test result", ex) } - }.start() + } } override fun isEnabled(@NotNull e: AnActionEvent): Boolean { @@ -202,8 +202,8 @@ class TestResultAutofixAction : BaseAction() { } private fun runAutofix(ui: ApplicationInterface, task: SessionTask) { - try { - Retryable(ui, task) { + Retryable(ui, task) { + try { val plan = ParsedActor( resultClass = ParsedErrors::class.java, prompt = """ @@ -220,6 +220,10 @@ class TestResultAutofixAction : BaseAction() { """.trimIndent(), model = AppSettingsState.instance.smartModel.chatModel() ).answer(listOf(testInfo), api = IdeaChatClient.instance) + if (plan.obj.errors.isNullOrEmpty()) { + task.add("No errors identified in test result") + return@Retryable "" + } task.add( AgentPatterns.displayMapInTabs( @@ -251,12 +255,15 @@ class TestResultAutofixAction : BaseAction() { } generateAndAddResponse(ui, task, error, summary, filesToFix) + return@Retryable "" } } - "" - } + return@Retryable "" } catch (e: Exception) { - task.error(ui, e) + log.error("Error in autofix process", e) + task.error(ui, e) + throw e + } } } @@ -266,7 +273,8 @@ class TestResultAutofixAction : BaseAction() { error: ParsedError, summary: String, filesToFix: List - ): String { + ) { + task.add("Generating fix suggestions...") val response = SimpleActor( prompt = """ You are a helpful AI that helps people with coding. @@ -286,20 +294,21 @@ $projectStructure """.trimIndent(), model = AppSettingsState.instance.smartModel.chatModel() ).answer(listOf(error.message ?: ""), api = IdeaChatClient.instance) + task.add("Processing suggested fixes...") var markdown = ui.socketManager?.addApplyFileDiffLinks( root = root.toPath(), response = response, handle = { newCodeMap -> newCodeMap.forEach { (path, newCode) -> + task.add("Applying changes to $path...") task.complete("$path Updated") } }, ui = ui, api = api, ) - val msg = "
${renderMarkdown(markdown!!)}
" - return msg + task.add("
${renderMarkdown(markdown!!)}
") } } 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 b6e1f160..cc6ea997 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt @@ -2,13 +2,15 @@ package com.github.simiacryptus.aicoder.config import com.github.simiacryptus.aicoder.util.UITools -open class AppSettingsConfigurable : UIAdapter(AppSettingsState.instance) { +open class AppSettingsConfigurable : UIAdapter( + AppSettingsState.instance +) { override fun read(component: AppSettingsComponent, settings: AppSettingsState) { - UITools.readKotlinUIViaReflection(component, settings) + UITools.readKotlinUIViaReflection(component, settings, AppSettingsComponent::class, AppSettingsState::class) } override fun write(settings: AppSettingsState, component: AppSettingsComponent) { - UITools.writeKotlinUIViaReflection(settings, component) + UITools.writeKotlinUIViaReflection(settings, component, AppSettingsState::class, AppSettingsComponent::class) } override fun getPreferredFocusedComponent() = component?.temperature diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt index 1652089d..ec44734b 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt @@ -81,11 +81,11 @@ abstract class UIAdapter( UITools.buildFormViaReflection(component, false)!! open fun read(component: C, settings: S) { - UITools.readKotlinUIViaReflection(component, settings) + UITools.readKotlinUIViaReflection(settings, component, Any::class, Any::class) } open fun write(settings: S, component: C) { - UITools.writeKotlinUIViaReflection(settings, component) + UITools.writeKotlinUIViaReflection(settings, component, Any::class, Any::class) } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt index 5e128cdc..91b2b531 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt @@ -480,4 +480,4 @@ enum class ComputerLanguage(configuration: Configuration) { return findByExtension(extension) } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/SessionManager.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/SessionManager.kt new file mode 100644 index 00000000..d135dec4 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/SessionManager.kt @@ -0,0 +1,16 @@ +package com.github.simiacryptus.aicoder.util + +import com.intellij.openapi.components.Service +import com.simiacryptus.skyenet.core.platform.Session + +@Service +class SessionManager { + private var currentSession: Session? = null + + fun createSession(): Session { + currentSession = Session.newGlobalID() // Or however you create your sessions + return currentSession!! + } + + fun getCurrentSession(): Session? = currentSession +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/StringUtil.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/StringUtil.kt new file mode 100644 index 00000000..4b114434 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/StringUtil.kt @@ -0,0 +1,21 @@ +package com.github.simiacryptus.aicoder.util + +object StringUtil { + fun lineWrapping(text: String, maxLineLength: Int): String { + val words = text.split(" ") + val lines = mutableListOf() + var currentLine = StringBuilder() + + for (word in words) { + if (currentLine.length + word.length + 1 <= maxLineLength) { + if (currentLine.isNotEmpty()) currentLine.append(" ") + currentLine.append(word) + } else { + if (currentLine.isNotEmpty()) lines.add(currentLine.toString()) + currentLine = StringBuilder(word) + } + } + if (currentLine.isNotEmpty()) lines.add(currentLine.toString()) + return lines.joinToString("\n") + } +} \ No newline at end of file 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 cb3946f7..9202c6bc 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.util +package com.github.simiacryptus.aicoder.util import com.github.simiacryptus.aicoder.actions.generic.toFile import com.github.simiacryptus.aicoder.config.AppSettingsState @@ -19,6 +19,7 @@ import com.intellij.openapi.progress.util.AbstractProgressIndicatorBase import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.TextRange import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.components.JBLabel @@ -31,7 +32,10 @@ import com.simiacryptus.jopenai.exceptions.ModerationException import com.simiacryptus.jopenai.models.APIProvider import org.jdesktop.swingx.JXButton import org.slf4j.LoggerFactory -import java.awt.* +import java.awt.BorderLayout +import java.awt.Component +import java.awt.Dimension +import java.awt.Toolkit import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import java.awt.event.WindowAdapter @@ -49,17 +53,39 @@ import java.util.function.Supplier import javax.swing.* import javax.swing.text.JTextComponent import kotlin.math.max +import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty1 import kotlin.reflect.KVisibility import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible -import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaType object UITools { val retry = WeakHashMap() + fun showError(project: Project?, message: String, title: String = "Error") { + Messages.showErrorDialog(project, message, title) + } + + fun showWarning(project: Project?, message: String, title: String = "Warning") { + Messages.showWarningDialog(project, message, title) + } + + fun getLanguageFromFile(fileName: String): String { + return when { + fileName.endsWith(".kt") -> "kotlin" + fileName.endsWith(".java") -> "java" + fileName.endsWith(".py") -> "python" + fileName.endsWith(".js") -> "javascript" + fileName.endsWith(".ts") -> "typescript" + fileName.endsWith(".html") -> "html" + fileName.endsWith(".css") -> "css" + fileName.endsWith(".xml") -> "xml" + fileName.endsWith(".json") -> "json" + else -> "text" + } + } private val log = LoggerFactory.getLogger(UITools::class.java) private val threadFactory: ThreadFactory = ThreadFactoryBuilder().setNameFormat("API Thread %d").build() private val pool: ListeningExecutorService by lazy { @@ -227,9 +253,13 @@ object UITools { return indent } - fun readKotlinUIViaReflection(component: R, settings: T) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = componentClass.kotlin.memberProperties.map { it.name }.toSet() + fun readKotlinUIViaReflection( + settings: T, + component: R, + componentClass: KClass<*>, + settingsClass: KClass<*> + ) { + val declaredUIFields = componentClass.memberProperties.map { it.name }.toSet() for (settingsField in settings.javaClass.kotlin.memberProperties) { if (settingsField is KMutableProperty<*>) { settingsField.isAccessible = true @@ -238,7 +268,7 @@ object UITools { var newSettingsValue: Any? = null if (!declaredUIFields.contains(settingsFieldName)) continue val uiField: KProperty1 = - (componentClass.kotlin.memberProperties.find { it.name == settingsFieldName } as KProperty1?)!! + (componentClass.memberProperties.find { it.name == settingsFieldName } as KProperty1?)!! var uiVal = uiField.get(component) if (uiVal is JScrollPane) { uiVal = uiVal.viewport.view @@ -293,9 +323,13 @@ object UITools { ) } - fun writeKotlinUIViaReflection(settings: T, component: R) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = componentClass.kotlin.memberProperties.map { it.name }.toSet() + fun writeKotlinUIViaReflection( + settings: T, + component: R, + componentClass: KClass<*>, + settingsClass: KClass<*> + ) { + val declaredUIFields = componentClass.memberProperties.map { it.name }.toSet() val memberProperties = settings.javaClass.kotlin.memberProperties val publicProperties = memberProperties.filter { it.visibility == KVisibility.PUBLIC //&& it is KMutableProperty<*> @@ -308,7 +342,7 @@ object UITools { continue } val uiField: KProperty1 = - (componentClass.kotlin.memberProperties.find { it.name == fieldName } as KProperty1?)!! + (componentClass.memberProperties.find { it.name == fieldName } as KProperty1?)!! val settingsVal = settingsField.get(settings) ?: continue var uiVal = uiField.get(component) if (uiVal is JScrollPane) { @@ -351,8 +385,7 @@ object UITools { private fun addKotlinFields(ui: T, formBuilder: FormBuilder, fillVertically: Boolean) { var first = true - for (field in ui.javaClass.kotlin.memberProperties) { - if (field.javaField == null) continue + for (field in ui.javaClass.kotlin.memberProperties.filterNotNull()) { try { val nameAnnotation = field.annotations.find { it is Name } as Name? val component = field.get(ui) as JComponent @@ -545,7 +578,7 @@ object UITools { configClass: Class, title: String = "Generate Project", onComplete: (C) -> Unit = { _ -> }, - ): C = showDialog( + ): C = showDialog( project, uiClass.getConstructor().newInstance(), configClass.getConstructor().newInstance(), @@ -580,7 +613,7 @@ object UITools { dialog.show() log.debug("Dialog shown with result: ${dialog.isOK}") if (dialog.isOK) { - readKotlinUIViaReflection(component, config) + readKotlinUIViaReflection(component, config, component::class, config::class) log.debug("Reading UI via reflection completed") onComplete(config) log.debug("onComplete callback executed") @@ -724,7 +757,7 @@ object UITools { } } - fun run( + fun run( project: Project?, title: String?, canBeCancelled: Boolean = true, @@ -945,11 +978,25 @@ object UITools { return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value } - fun showErrorDialog(project: Project?, errorMessage: String, subMessage: String) { + fun showErrorDialog(project: Project?, errorMessage: String, title: String) { val formBuilder = FormBuilder.createFormBuilder() - formBuilder.addLabeledComponent("Error", JLabel(errorMessage)) - formBuilder.addLabeledComponent("Details", JLabel(subMessage)) - showOptionDialog(formBuilder.panel, "Dismiss", title = "Error", modal = true) + formBuilder.addComponent(JLabel(errorMessage)) + showOptionDialog(formBuilder.panel, "OK", title = title, modal = true) } + fun showErrorDetails(project: Project?, e: Throwable) { + val sw = StringWriter() + e.printStackTrace(PrintWriter(sw)) + val formBuilder = FormBuilder.createFormBuilder() + val textArea = JBTextArea(sw.toString()) + textArea.isEditable = false + formBuilder.addComponent(JBScrollPane(textArea)) + showOptionDialog(formBuilder.panel, "OK", title = "Error Details", modal = true) + } + + fun showInfoMessage(project: Project?, message: String, title: String) { + val formBuilder = FormBuilder.createFormBuilder() + formBuilder.addComponent(JLabel(message)) + showOptionDialog(formBuilder.panel, "OK", title = title, modal = true) + } } \ No newline at end of file