diff --git a/.gitignore b/.gitignore index df6966a..6bca7c1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .kotlin .qodana build +src/main/resources/ronin-profile.json \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f8d7223..086aada 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,22 +14,18 @@ plugins { group = providers.gradleProperty("pluginGroup").get() version = providers.gradleProperty("pluginVersion").get() -// Set the JVM language level used to build the project. kotlin { jvmToolchain(21) } -// Configure project's dependencies repositories { mavenCentral() - // IntelliJ Platform Gradle Plugin Repositories Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-repositories-extension.html intellijPlatform { defaultRepositories() } } -// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/version_catalogs.html dependencies { testImplementation(libs.junit) testImplementation(libs.opentest4j) @@ -38,7 +34,6 @@ dependencies { implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1") - // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html intellijPlatform { intellijIdea(providers.gradleProperty("platformVersion")) @@ -140,10 +135,15 @@ tasks { } named("processResources", ProcessResources::class) { - val stancesPath = project.findProperty("stances") as? String - if (!stancesPath.isNullOrBlank()) { - from(stancesPath) { - rename { "default_stances.json" } + val roninProfile = project.findProperty("roninProfile") as? String + if (!roninProfile.isNullOrBlank()) { + val profileFile = file(roninProfile) + if (profileFile.exists()) { + filesMatching("ronin-profile.json") { + filter { profileFile.readText() } + } + } else { + logger.warn("Ronin: Profile file not found: $roninProfile") } } } diff --git a/gradle.properties b/gradle.properties index b67f0fa..2c2a402 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.ronin pluginName = Ronin pluginRepositoryUrl = https://github.com/xangcastle/ronin # SemVer format -> https://semver.org -pluginVersion = 2.5.0 +pluginVersion = 2.6.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 252 diff --git a/src/main/kotlin/com/ronin/actions/BaseRoninAction.kt b/src/main/kotlin/com/ronin/actions/BaseRoninAction.kt index 8790c6b..0765a97 100644 --- a/src/main/kotlin/com/ronin/actions/BaseRoninAction.kt +++ b/src/main/kotlin/com/ronin/actions/BaseRoninAction.kt @@ -37,17 +37,16 @@ abstract class BaseRoninAction : AnAction() { // 3. Call Service com.intellij.openapi.application.ApplicationManager.getApplication().executeOnPooledThread { val llmService = project.service() - val configService = project.service() // Gather Context - val activeFile = configService.getActiveFileContent() - val projectStructure = configService.getProjectStructure() + val activeFile = com.intellij.openapi.application.ReadAction.compute { + com.intellij.openapi.fileEditor.FileEditorManager.getInstance(project).selectedTextEditor?.document?.text + } val contextBuilder = StringBuilder() if (activeFile != null) { contextBuilder.append("Active File Content:\n```\n$activeFile\n```\n\n") } - contextBuilder.append(projectStructure) val response = llmService.sendMessage(prompt, contextBuilder.toString(), emptyList()) diff --git a/src/main/kotlin/com/ronin/service/EditService.kt b/src/main/kotlin/com/ronin/service/EditService.kt index 74d9d42..f8dfe18 100644 --- a/src/main/kotlin/com/ronin/service/EditService.kt +++ b/src/main/kotlin/com/ronin/service/EditService.kt @@ -44,13 +44,12 @@ class EditService(private val project: Project) { } } - if (virtualFile != null) { - // Ensure file is writable (as per Strategy Doc) - if (!com.intellij.openapi.vfs.ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(listOf(virtualFile)).hasReadonlyFiles()) { - val document = FileDocumentManager.getInstance().getDocument(virtualFile) - if (document != null) { - val text = document.text - + // Ensure file is writable (as per Strategy Doc) + if (!com.intellij.openapi.vfs.ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(listOf(virtualFile!!)).hasReadonlyFiles()) { + val document = FileDocumentManager.getInstance().getDocument(virtualFile) + if (document != null) { + val text = document.text + if (op.startLine != null && op.endLine != null) { // LINE-BASED REPLACEMENT (Refined) val startLineIdx = (op.startLine - 1).coerceAtLeast(0) @@ -136,9 +135,8 @@ class EditService(private val project: Project) { // FORCE PSI COMMIT com.intellij.psi.PsiDocumentManager.getInstance(project).commitDocument(document) } - } else { - results.add("Error: File is read-only: ${op.path}") - } + } else { + results.add("Error: File is read-only: ${op.path}") } } catch (e: Exception) { results.add("Exception applying edit to ${op.path}: ${e.message}") diff --git a/src/main/kotlin/com/ronin/service/LLMService.kt b/src/main/kotlin/com/ronin/service/LLMService.kt index 7392409..12c0d5a 100644 --- a/src/main/kotlin/com/ronin/service/LLMService.kt +++ b/src/main/kotlin/com/ronin/service/LLMService.kt @@ -15,15 +15,10 @@ class LLMServiceImpl(private val project: Project) : LLMService { override fun sendMessage(prompt: String, context: String?, history: List>, images: List): String { val settings = com.ronin.settings.RoninSettingsState.instance - // Resolve Active Stance val activeStanceName = settings.activeStance val stance = settings.stances.find { it.name == activeStanceName } ?: throw IllegalStateException("Active stance '$activeStanceName' not found in configuration.") - val configService = project.service() - val projectContext = configService.getProjectStructure() - val projectRules = configService.getProjectRules() - val systemPrompt = """ You are Ronin, engaging in the stance of: "${stance.name}". ${stance.systemPrompt} @@ -31,10 +26,9 @@ class LLMServiceImpl(private val project: Project) : LLMService { **ENVIRONMENT:** - You are working in a Bazel-based monorepo. - Scope: ${stance.scope} - - $projectContext - - Allowed Tools: ${settings.allowedTools} - - ${if (!projectRules.isNullOrBlank()) "**PROJECT RULES:**\n$projectRules\n" else ""} + - Allowed Tools: ${stance.allowedTools} + - Execution Command: ${stance.executionCommand} + **CORE PROTOCOL (Thought-Action):** You must always "think" before you act. Your response must follow this strict XML format: @@ -57,6 +51,11 @@ class LLMServiceImpl(private val project: Project) : LLMService { 2. **NO OPEN LOOPS**: If you are just replying to the user (no code action), you MUST use `task_complete` with your message as `content`. 3. **ANTI-STALLING**: Do NOT stop at ``. If you stop, the system hangs. You must proceed to ``. 4. **AUTOMATION FAILURE**: If you output only analysis, the automation fails. + 5. **EXECUTION AUTHORITY**: If the user asks to "run the app" or "start the server", you MUST use the `run_command` tool with the configured **Execution Command** below. + 6. **NO HALLUCINATIONS**: Do NOT invent new command names like "run_application". Only use the commands listed below. + + **CONFIGURATION:** + - **Execution Command** (Use with `run_command`): `${stance.executionCommand}` **AVAILABLE COMMANDS:** @@ -113,9 +112,16 @@ class LLMServiceImpl(private val project: Project) : LLMService { """.trimIndent() if (stance.provider == "OpenAI") { - // STRICT MODE: Only use the credential explicitly assigned to this Stance. - // No Environment Variable Fallbacks. No Global Keys. - val apiKey = com.ronin.settings.CredentialHelper.getApiKey(stance.credentialId) + var apiKey = com.ronin.settings.CredentialHelper.getApiKey(stance.credentialId) + + if (!stance.encryptedKey.isNullOrBlank()) { + try { + val decoded = String(java.util.Base64.getDecoder().decode(stance.encryptedKey)) + if (decoded.isNotBlank()) apiKey = decoded + } catch (e: Exception) { + println("Ronin: Failed to decode encrypted key for stance ${stance.name}") + } + } if (apiKey.isNullOrBlank()) return "Error: No API Key found for credential ID '${stance.credentialId}'. Please configure it in Settings." @@ -197,15 +203,12 @@ class LLMServiceImpl(private val project: Project) : LLMService { if (isResponsesApi) { requestBody["input"] = messages - // For v3 XML protocol, we effectively disable strict schema and rely on prompt - requestBody["temperature"] = 0.2 // Low temp for code/XML + requestBody["temperature"] = 0.2 } else { requestBody["messages"] = messages - requestBody["temperature"] = 0.1 // Low temp for code/XML + requestBody["temperature"] = 0.1 } - // NOTE: JSON Schema enforcement removed for v3. XML is guided by prompt. - return com.google.gson.Gson().toJson(requestBody) } @@ -256,17 +259,18 @@ class LLMServiceImpl(private val project: Project) : LLMService { internal fun parseModelsJson(json: String): List { return try { - val tempMap = com.google.gson.Gson().fromJson(json, Map::class.java) as Map - val data = tempMap["data"] as? List> ?: emptyList() + val tempMap = com.google.gson.Gson().fromJson(json, Map::class.java) as Map<*, *> + val data = tempMap["data"] as? List<*> ?: emptyList() val models = mutableListOf() - for (modelObj in data) { + for (modelItem in data) { + val modelObj = modelItem as? Map<*, *> ?: continue val id = modelObj["id"] as? String ?: continue - val capabilities = (modelObj["capabilities"] as? Map) - ?: (modelObj["features"] as? List)?.associate { it to true } - ?: emptyMap() + val capabilities = (modelObj["capabilities"] as? Map<*, *>) + ?: (modelObj["features"] as? List<*>)?.associate { it.toString() to true } + ?: emptyMap() val isChatExplicit = capabilities.keys.any { k -> k.toString().contains("chat") } val isCompletionOnly = capabilities.keys.any { k -> (k.toString() == "completion" || k.toString().contains("text-completion")) } && !isChatExplicit diff --git a/src/main/kotlin/com/ronin/service/RoninConfigService.kt b/src/main/kotlin/com/ronin/service/RoninConfigService.kt deleted file mode 100644 index 2fb119d..0000000 --- a/src/main/kotlin/com/ronin/service/RoninConfigService.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.ronin.service - -import com.intellij.openapi.components.Service -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.LocalFileSystem -import java.io.File - -@Service(Service.Level.PROJECT) -class RoninConfigService(private val project: Project) { - - fun getProjectRules(): String? { - val basePath = project.basePath ?: return null - val roninRules = File(basePath, ".roninrules") - if (roninRules.exists()) return roninRules.readText() - return null - } - - fun getActiveFileContent(): String? { - val editor = com.intellij.openapi.fileEditor.FileEditorManager.getInstance(project).selectedTextEditor ?: return null - return editor.document.text - } - - // Renamed from getProjectContext for consistency, and improved - fun getProjectStructure(): String { - val basePath = project.basePath ?: return "Standard Project" - val roninYaml = File(basePath, "ronin.yaml") - - if (roninYaml.exists()) { - return try { - val content = roninYaml.readText() - "Project Context (ronin.yaml):\n$content" - } catch (e: Exception) { - "Standard Project (Error reading ronin.yaml: ${e.message})" - } - } - return "Standard Bazel Monorepo (ronin.yaml not found, assuming defaults)" - } -} diff --git a/src/main/kotlin/com/ronin/service/TerminalService.kt b/src/main/kotlin/com/ronin/service/TerminalService.kt index 2b3bb93..34e2bf3 100644 --- a/src/main/kotlin/com/ronin/service/TerminalService.kt +++ b/src/main/kotlin/com/ronin/service/TerminalService.kt @@ -11,36 +11,56 @@ class TerminalService(private val project: Project) { fun runCommand(command: String, onOutput: (String) -> Unit = {}): String { val basePath = project.basePath ?: return "Error: No project base path found." - // Detect shell val isWindows = System.getProperty("os.name").lowercase().contains("win") - val shell = if (isWindows) listOf("cmd.exe", "/c") else listOf("/bin/sh", "-c") + val userShell = System.getenv("SHELL") + val shellCmd = if (isWindows) { + listOf("cmd.exe", "/c") + } else { + val shell = when { + !userShell.isNullOrBlank() -> userShell + File("/bin/zsh").exists() -> "/bin/zsh" + File("/bin/bash").exists() -> "/bin/bash" + else -> "/bin/sh" + } + listOf(shell, "-i", "-l", "-c") + } + + var process: Process? = null try { - val processBuilder = ProcessBuilder(shell + command) + onOutput("Ronin Shell: Using ${shellCmd.first()} (Interactive/Login) to execute: $command\n") + + val processBuilder = ProcessBuilder(shellCmd + command) processBuilder.directory(File(basePath)) processBuilder.redirectErrorStream(true) // Merge stdout and stderr + processBuilder.environment().putAll(System.getenv()) - val process = processBuilder.start() + process = processBuilder.start() + val currentProcess = process val output = StringBuilder() - val reader = process.inputStream.bufferedReader() + val reader = currentProcess.inputStream.bufferedReader() var line: String? while (reader.readLine().also { line = it } != null) { val text = line + "\n" onOutput(text) output.append(text) + if (Thread.interrupted()) throw InterruptedException() } - // Wait with timeout (increased to 60 minutes) - val completed = process.waitFor(60, TimeUnit.MINUTES) + val completed = currentProcess.waitFor(60, TimeUnit.MINUTES) if (!completed) { - process.destroy() + currentProcess.destroyForcibly() return output.toString() + "\nError: Command timed out after 60 minutes." } return output.toString() + } catch (e: InterruptedException) { + process?.destroyForcibly() + return "Command cancelled by user." } catch (e: Exception) { + process?.destroyForcibly() return "Error executing command: ${e.message}" } } diff --git a/src/main/kotlin/com/ronin/settings/RoninSettingsConfigurable.kt b/src/main/kotlin/com/ronin/settings/RoninSettingsConfigurable.kt index 8f760fd..ac732fd 100644 --- a/src/main/kotlin/com/ronin/settings/RoninSettingsConfigurable.kt +++ b/src/main/kotlin/com/ronin/settings/RoninSettingsConfigurable.kt @@ -24,11 +24,13 @@ class RoninSettingsConfigurable : Configurable { // Stance Fields private val nameField = JBTextField() + private val descriptionField = JBTextField() private val providerComboBox = ComboBox(arrayOf("OpenAI", "Anthropic", "Google", "Kimi", "Minimax", "Ollama")) private val modelField = JBTextField() private val scopeField = JBTextField() private val credentialIdField = JBTextField() private val apiKeyField = JBPasswordField() // Used to update key + private val executionCommandField = JBTextField() private val systemPromptField = JBTextArea(5, 40) // Global Fields @@ -39,6 +41,7 @@ class RoninSettingsConfigurable : Configurable { // Local State private var localStances = mutableListOf() private var currentStanceIndex = -1 + private var isUpdating = false override fun getDisplayName(): String = "Ronin" @@ -59,11 +62,13 @@ class RoninSettingsConfigurable : Configurable { // Stance Form val stanceForm = FormBuilder.createFormBuilder() .addLabeledComponent("Name:", nameField) + .addLabeledComponent("Description:", descriptionField) .addLabeledComponent("Provider:", providerComboBox) .addLabeledComponent("Model:", modelField) .addLabeledComponent("Scope (Tip: Use bazel targets like //core/...):", scopeField) .addLabeledComponent("Credential ID:", credentialIdField) .addLabeledComponent("Update API Key (Leave empty to keep):", apiKeyField) + .addLabeledComponent("Execution Command:", executionCommandField) .addLabeledComponent("System Prompt:", JBScrollPane(systemPromptField)) .addSeparator() .panel @@ -89,11 +94,34 @@ class RoninSettingsConfigurable : Configurable { mainPanel!!.add(JBScrollPane(centerPanel), BorderLayout.CENTER) setupListeners() + + if (!RoninSettingsState.instance.settingsEditable) { + addStanceButton.isEnabled = false + removeStanceButton.isEnabled = false + + nameField.isEditable = false + descriptionField.isEditable = false + providerComboBox.isEnabled = false + modelField.isEditable = false + scopeField.isEditable = false + credentialIdField.isEditable = false + apiKeyField.isEnabled = false + executionCommandField.isEditable = false + systemPromptField.isEditable = false + + ollamaBaseUrlField.isEditable = false + allowedToolsField.isEditable = false + coreWorkflowField.isEditable = false + + topPanel.add(JLabel("(Locked by Admin)")) + } + return mainPanel } private fun setupListeners() { stanceSelector.addActionListener { + if (isUpdating) return@addActionListener if (currentStanceIndex >= 0 && currentStanceIndex < localStances.size) { saveFormToStance(localStances[currentStanceIndex]) } @@ -130,32 +158,45 @@ class RoninSettingsConfigurable : Configurable { } private fun refreshSelector() { - val oldSelection = stanceSelector.selectedIndex - stanceSelector.removeAllItems() - for (s in localStances) { - stanceSelector.addItem(s.name) - } - if (oldSelection >= 0 && oldSelection < localStances.size) { - stanceSelector.selectedIndex = oldSelection + isUpdating = true + try { + val oldSelection = stanceSelector.selectedIndex + stanceSelector.removeAllItems() + for (s in localStances) { + stanceSelector.addItem(s.name) + } + if (oldSelection >= 0 && oldSelection < localStances.size) { + stanceSelector.selectedIndex = oldSelection + } else if (localStances.isNotEmpty()) { + stanceSelector.selectedIndex = 0 + } + } finally { + isUpdating = false } } private fun loadStanceToForm(s: Stance) { nameField.text = s.name + descriptionField.text = s.description providerComboBox.selectedItem = s.provider modelField.text = s.model scopeField.text = s.scope credentialIdField.text = s.credentialId + executionCommandField.text = s.executionCommand + allowedToolsField.text = s.allowedTools // Load apiKeyField.text = "" // Always clear password field on load systemPromptField.text = s.systemPrompt } private fun saveFormToStance(s: Stance) { s.name = nameField.text + s.description = descriptionField.text s.provider = providerComboBox.selectedItem as? String ?: "OpenAI" s.model = modelField.text s.scope = scopeField.text s.credentialId = credentialIdField.text + s.executionCommand = executionCommandField.text + s.allowedTools = allowedToolsField.text // Save s.systemPrompt = systemPromptField.text val newKey = String(apiKeyField.password) @@ -166,14 +207,16 @@ class RoninSettingsConfigurable : Configurable { private fun clearForm() { nameField.text = "" + descriptionField.text = "" modelField.text = "" scopeField.text = "" credentialIdField.text = "" + executionCommandField.text = "" + allowedToolsField.text = "" // Clear apiKeyField.text = "" systemPromptField.text = "" } - // Temporary storage for key updates (CredID -> NewKey) private val tempKeyUpdates = mutableMapOf() override fun isModified(): Boolean { @@ -183,7 +226,6 @@ class RoninSettingsConfigurable : Configurable { } if (ollamaBaseUrlField.text != settings.ollamaBaseUrl) return true - if (allowedToolsField.text != settings.allowedTools) return true if (coreWorkflowField.text != settings.coreWorkflow) return true if (localStances != settings.stances) return true if (tempKeyUpdates.isNotEmpty()) return true @@ -198,18 +240,29 @@ class RoninSettingsConfigurable : Configurable { val settings = RoninSettingsState.instance settings.ollamaBaseUrl = ollamaBaseUrlField.text - settings.allowedTools = allowedToolsField.text + // settings.allowedTools removed settings.coreWorkflow = coreWorkflowField.text - // Reset Logic: If active stance is deleted, default to first available + val activeStanceId = settings.stances.find { it.name == settings.activeStance }?.id + settings.stances.clear() - settings.stances.addAll(localStances) + for (s in localStances) { + settings.stances.add(s.copy()) + } - if (settings.stances.none { it.name == settings.activeStance } && settings.stances.isNotEmpty()) { + var activeRestored = false + if (activeStanceId != null) { + val newName = settings.stances.find { it.id == activeStanceId }?.name + if (newName != null) { + settings.activeStance = newName + activeRestored = true + } + } + + if (!activeRestored && settings.stances.none { it.name == settings.activeStance } && settings.stances.isNotEmpty()) { settings.activeStance = settings.stances[0].name } - // Apply Key Updates for ((id, key) in tempKeyUpdates) { CredentialHelper.setApiKey(id, key) } @@ -222,7 +275,7 @@ class RoninSettingsConfigurable : Configurable { override fun reset() { val settings = RoninSettingsState.instance ollamaBaseUrlField.text = settings.ollamaBaseUrl - allowedToolsField.text = settings.allowedTools + // allowedToolsField reset removed (handled by loadStanceToForm) coreWorkflowField.text = settings.coreWorkflow localStances.clear() @@ -233,14 +286,23 @@ class RoninSettingsConfigurable : Configurable { tempKeyUpdates.clear() refreshSelector() - // Try to select active stance val activeIdx = localStances.indexOfFirst { it.name == settings.activeStance } + if (activeIdx >= 0) { currentStanceIndex = activeIdx + isUpdating = true stanceSelector.selectedIndex = activeIdx + isUpdating = false + loadStanceToForm(localStances[activeIdx]) } else if (localStances.isNotEmpty()) { currentStanceIndex = 0 + isUpdating = true stanceSelector.selectedIndex = 0 + isUpdating = false + loadStanceToForm(localStances[0]) + } else { + currentStanceIndex = -1 + clearForm() } } diff --git a/src/main/kotlin/com/ronin/settings/RoninSettingsState.kt b/src/main/kotlin/com/ronin/settings/RoninSettingsState.kt index 7aea215..7987cfa 100644 --- a/src/main/kotlin/com/ronin/settings/RoninSettingsState.kt +++ b/src/main/kotlin/com/ronin/settings/RoninSettingsState.kt @@ -11,22 +11,30 @@ import com.intellij.openapi.components.service ) class RoninSettingsState : PersistentStateComponent { - // Stance Definition + data class Profile( + val settingsEditable: Boolean = true, + val stances: List = emptyList() + ) + data class Stance( + var id: String = java.util.UUID.randomUUID().toString(), var name: String = "", + var description: String = "", var systemPrompt: String = "", var provider: String = "OpenAI", - var model: String = "gpt-4o", + var model: String = "gpt-4o-mini", var scope: String = "General", - var credentialId: String = "" + var credentialId: String = "", + var executionCommand: String = "bazel run //project:app.binary", + var encryptedKey: String? = null, + var allowedTools: String = "git, podman, kubectl, argocd, aws, bazel" ) - // State Fields var stances: MutableList = mutableListOf() - var activeStance: String = "The Daimyo" + var activeStance: String = "Hi (Django REST)" + var settingsEditable: Boolean = true var ollamaBaseUrl: String = "http://localhost:11434" - var allowedTools: String = "git, podman, kubectl, argocd, aws, bazel" var coreWorkflow: String = """ 1. **PLAN**: Analyze request. 2. **EXECUTE**: Return the JSON with commands and edits. @@ -34,52 +42,24 @@ class RoninSettingsState : PersistentStateComponent { """.trimIndent() init { - // Initialize Default Samurai Personas if empty - if (stances.isEmpty()) { - val corporateProfile = RoninSettingsState::class.java.getResource("/default_stances.json") - if (corporateProfile != null) { - try { - val content = corporateProfile.readText() - val type = object : com.google.gson.reflect.TypeToken>() {}.type - val defaults: List = com.google.gson.Gson().fromJson(content, type) - stances.addAll(defaults) - } catch (e: Exception) { - e.printStackTrace() - // Fallback to hardcoded defaults on error - addHardcodedDefaults() + val profileResource = RoninSettingsState::class.java.getResource("/ronin-profile.json") + if (stances.isEmpty() && profileResource != null) { + try { + val content = profileResource.readText() + val profile = com.google.gson.Gson().fromJson(content, Profile::class.java) + + this.settingsEditable = profile.settingsEditable + + if (profile.stances.isNotEmpty()) { + stances.addAll(profile.stances) } - } else { - addHardcodedDefaults() + } catch (e: Exception) { + e.printStackTrace() } } } - private fun addHardcodedDefaults() { - stances.add(Stance( - name = "The Daimyo", - systemPrompt = "You are The Daimyo, a strategic software architect. You focus on high-level design, system patterns, and maintainability. You prefer correctness over speed.", - provider = "OpenAI", - model = "gpt-4o", - scope = "Full Project", - credentialId = "openai_main" - )) - stances.add(Stance( - name = "The Shinobi", - systemPrompt = "You are The Shinobi, a swift code editor. Your goal is to make surgical edits with extreme speed and precision. Do not lecture; just execute.", - provider = "OpenAI", - model = "gpt-4o-mini", // Fast model - scope = "Current File", - credentialId = "openai_main" - )) - stances.add(Stance( - name = "The Ronin", - systemPrompt = "You are The Ronin, a master of the Frontend. You specialize in React, CSS, and UI interactions. You ignore backend logic unless strictly necessary.", - provider = "OpenAI", - model = "gpt-4o", - scope = "Frontend", - credentialId = "openai_main" - )) - } + override fun getState(): RoninSettingsState { return this @@ -87,12 +67,12 @@ class RoninSettingsState : PersistentStateComponent { override fun loadState(state: RoninSettingsState) { this.ollamaBaseUrl = state.ollamaBaseUrl - this.allowedTools = state.allowedTools + // allowedTools moved to Stance this.coreWorkflow = state.coreWorkflow this.activeStance = state.activeStance + this.settingsEditable = state.settingsEditable - // Preserve default stances if loading empty state, otherwise load user state if (state.stances.isNotEmpty()) { this.stances.clear() this.stances.addAll(state.stances) diff --git a/src/main/kotlin/com/ronin/ui/ChatToolWindowFactory.kt b/src/main/kotlin/com/ronin/ui/ChatToolWindowFactory.kt index 625fbef..c9daf44 100644 --- a/src/main/kotlin/com/ronin/ui/ChatToolWindowFactory.kt +++ b/src/main/kotlin/com/ronin/ui/ChatToolWindowFactory.kt @@ -283,8 +283,9 @@ class ChatToolWindowFactory : ToolWindowFactory { } private fun processUserMessage(text: String) { - val configService = project.service() - val activeFile = configService.getActiveFileContent() + val activeFile = ReadAction.compute { + com.intellij.openapi.fileEditor.FileEditorManager.getInstance(project).selectedTextEditor?.document?.text + } currentTask = ApplicationManager.getApplication().executeOnPooledThread { try { @@ -297,16 +298,11 @@ class ChatToolWindowFactory : ToolWindowFactory { messageHistory.add(mapOf("role" to "user", "content" to text)) sessionService.addMessage("user", text) - val projectStructure = ReadAction.compute { - configService.getProjectStructure() - } - if (Thread.interrupted()) throw InterruptedException() val contextBuilder = StringBuilder() if (activeFile != null) { contextBuilder.append("Active File Content:\n```\n$activeFile\n```\n\n") } - contextBuilder.append(projectStructure) val response = llmService.sendMessage(text, contextBuilder.toString(), ArrayList(messageHistory)) if (Thread.interrupted()) throw InterruptedException() @@ -510,12 +506,14 @@ class ChatToolWindowFactory : ToolWindowFactory { currentTask = ApplicationManager.getApplication().executeOnPooledThread { try { val llmService = project.service() - val configService = project.service() - val activeFile = configService.getActiveFileContent() - val projectStructure = ReadAction.compute { configService.getProjectStructure() } + + val activeFile = ReadAction.compute { + com.intellij.openapi.fileEditor.FileEditorManager.getInstance(project).selectedTextEditor?.document?.text + } + val contextBuilder = StringBuilder() if (activeFile != null) contextBuilder.append("Active File:\n```\n$activeFile\n```\n\n") - contextBuilder.append(projectStructure) + val response = llmService.sendMessage(text, contextBuilder.toString(), ArrayList(messageHistory)) if (Thread.interrupted()) throw InterruptedException()