Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
.kotlin
.qodana
build
src/main/resources/ronin-profile.json
src/main/resources/stances
95 changes: 1 addition & 94 deletions src/main/kotlin/com/ronin/service/LLMService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,102 +14,11 @@ class LLMServiceImpl(private val project: Project) : LLMService {

override fun sendMessage(prompt: String, context: String?, history: List<Map<String, String>>, images: List<String>): String {
val settings = com.ronin.settings.RoninSettingsState.instance

val activeStanceName = settings.activeStance
val stance = settings.stances.find { it.name == activeStanceName }
?: throw IllegalStateException("Active stance '$activeStanceName' not found in configuration.")

val systemPrompt = """
You are Ronin, engaging in the stance of: "${stance.name}".
${stance.systemPrompt}

**ENVIRONMENT:**
- You are working in a Bazel-based monorepo.
- Scope: ${stance.scope}
- 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:

<analysis>
1. Analyze the user request and file context.
2. Plan your specific edits or actions.
3. Verify your plan against project rules.
</analysis>

<execute>
<!-- YOU MUST PROVIDE EXACTLY ONE COMMAND HERE. DO NOT LEAVE EMPTY. -->
<command name="COMMAND_NAME">
<arg name="ARG_NAME">ARG_VALUE</arg>
</command>
</execute>

**CRITICAL RULES:**
1. **MANDATORY EXECUTION**: You MUST output an `<execute>` block in every single turn.
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 `<analysis>`. If you stop, the system hangs. You must proceed to `<execute>`.
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:**

1. `read_code`: Inspect files.
<command name="read_code">
<arg name="path">libs/core/utils.py</arg>
<arg name="start_line">1</arg> <!-- Optional, default 1 -->
<arg name="end_line">100</arg> <!-- Optional, default 500 lines -->
</command>

2. `write_code`: Modify files. Use CDATA for content to avoid escaping issues.
<command name="write_code">
<arg name="path">libs/core/utils.py</arg>
<!-- OPTION A: Line-Based Replacement (Preferred) -->
<arg name="start_line">10</arg>
<arg name="end_line">15</arg>
<content><![CDATA[
def new_function():
return True
]]></content>
</command>

OR

<command name="write_code">
<arg name="path">...</arg>
<!-- OPTION B: Search & Replace (Fuzzy) -->
<arg name="code_search"><![CDATA[def old_function():]]></arg>
<content><![CDATA[def new_function():]]></content>
</command>

3. `run_command`: Execute shell commands.
<command name="run_command">
<arg name="command">./gradlew build</arg>
</command>

4. `task_complete`: Signal completion.
<command name="task_complete">
<arg name="content">I have finished the task.</arg>
</command>

**INSTRUCTIONS:**
- **MODE: VIBE CODING (Architect/Editor)**:
- **Self-Healing**: If you make a mistake, analyze it in <analysis> and fix it in the next turn.
- **Context Echo**: After editing, the tool returns the result. READ IT.
- **SAFE EDITING**:
- Use `write_code` with `start_line`/`end_line` whenever possible.
- **STRICT ANTI-REPETITION**: If a command fails, do not retry blindly. Read the file, understand the state, then fix.

User Request: $prompt

(REMINDER: You MUST end your response with an <execute> block containing a command. Do not just analyze.)
Context: $context
""".trimIndent()
val systemPrompt = stance.systemPrompt.trimIndent()

if (stance.provider == "OpenAI") {
var apiKey = com.ronin.settings.CredentialHelper.getApiKey(stance.credentialId)
Expand All @@ -132,12 +41,10 @@ class LLMServiceImpl(private val project: Project) : LLMService {

private fun sendOpenAIRequest(systemPrompt: String, history: List<Map<String, String>>, model: String, apiKey: String, enforceJson: Boolean): String {

// Prune history to manage token limits (approx heuristic)
val prunedHistory = pruneHistory(history, 20)

val jsonBody = createOpenAIRequestBody(model, systemPrompt, prunedHistory, enforceJson)

// Custom endpoint handling for specific internal/preview models
val endpoint = if (model.contains("codex") || model.contains("gpt-5.1")) {
"https://api.openai.com/v1/responses"
} else {
Expand Down
28 changes: 0 additions & 28 deletions src/main/kotlin/com/ronin/settings/RoninSettingsConfigurable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,12 @@ class RoninSettingsConfigurable : Configurable {
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
private val ollamaBaseUrlField = JBTextField()
private val allowedToolsField = JBTextField()
private val coreWorkflowField = JBTextArea(5, 40)

// Local State
private var localStances = mutableListOf<Stance>()
Expand All @@ -48,8 +44,6 @@ class RoninSettingsConfigurable : Configurable {
override fun createComponent(): JComponent? {
systemPromptField.lineWrap = true
systemPromptField.wrapStyleWord = true
coreWorkflowField.lineWrap = true
coreWorkflowField.wrapStyleWord = true

// Top Bar: Selector + Buttons
val topPanel = JPanel(java.awt.FlowLayout(java.awt.FlowLayout.LEFT))
Expand All @@ -65,10 +59,8 @@ class RoninSettingsConfigurable : Configurable {
.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
Expand All @@ -78,8 +70,6 @@ class RoninSettingsConfigurable : Configurable {
.addSeparator()
.addSeparator()
.addLabeledComponent(MyBundle.message("settings.ollama_url"), ollamaBaseUrlField)
.addLabeledComponent(MyBundle.message("settings.allowed_tools"), allowedToolsField)
.addLabeledComponent(MyBundle.message("settings.core_workflow"), JBScrollPane(coreWorkflowField))
.addComponentFillVertically(JPanel(), 0)
.panel

Expand All @@ -103,15 +93,11 @@ class RoninSettingsConfigurable : Configurable {
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("<html><font color='gray'>(Locked by Admin)</font></html>"))
}
Expand Down Expand Up @@ -180,10 +166,7 @@ class RoninSettingsConfigurable : Configurable {
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
}
Expand All @@ -193,10 +176,7 @@ class RoninSettingsConfigurable : Configurable {
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)
Expand All @@ -209,10 +189,7 @@ class RoninSettingsConfigurable : Configurable {
nameField.text = ""
descriptionField.text = ""
modelField.text = ""
scopeField.text = ""
credentialIdField.text = ""
executionCommandField.text = ""
allowedToolsField.text = "" // Clear
apiKeyField.text = ""
systemPromptField.text = ""
}
Expand All @@ -226,7 +203,6 @@ class RoninSettingsConfigurable : Configurable {
}

if (ollamaBaseUrlField.text != settings.ollamaBaseUrl) return true
if (coreWorkflowField.text != settings.coreWorkflow) return true
if (localStances != settings.stances) return true
if (tempKeyUpdates.isNotEmpty()) return true

Expand All @@ -240,8 +216,6 @@ class RoninSettingsConfigurable : Configurable {

val settings = RoninSettingsState.instance
settings.ollamaBaseUrl = ollamaBaseUrlField.text
// settings.allowedTools removed
settings.coreWorkflow = coreWorkflowField.text

val activeStanceId = settings.stances.find { it.name == settings.activeStance }?.id

Expand Down Expand Up @@ -275,8 +249,6 @@ class RoninSettingsConfigurable : Configurable {
override fun reset() {
val settings = RoninSettingsState.instance
ollamaBaseUrlField.text = settings.ollamaBaseUrl
// allowedToolsField reset removed (handled by loadStanceToForm)
coreWorkflowField.text = settings.coreWorkflow

localStances.clear()
for (s in settings.stances) {
Expand Down
98 changes: 77 additions & 21 deletions src/main/kotlin/com/ronin/settings/RoninSettingsState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)
class RoninSettingsState : PersistentStateComponent<RoninSettingsState> {

data class Profile(

Check warning on line 14 in src/main/kotlin/com/ronin/settings/RoninSettingsState.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Class "Profile" is never used

Check warning on line 14 in src/main/kotlin/com/ronin/settings/RoninSettingsState.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Class "Profile" is never used
val settingsEditable: Boolean = true,
val stances: List<Stance> = emptyList()
)
Expand All @@ -23,39 +23,97 @@
var systemPrompt: String = "",
var provider: String = "OpenAI",
var model: String = "gpt-4o-mini",
var scope: String = "General",
var credentialId: String = "",
var executionCommand: String = "bazel run //project:app.binary",
var encryptedKey: String? = null,
var allowedTools: String = "git, podman, kubectl, argocd, aws, bazel"
var encryptedKey: String? = null
)

var stances: MutableList<Stance> = mutableListOf()
var activeStance: String = "Hi (Django REST)"
var settingsEditable: Boolean = true

var ollamaBaseUrl: String = "http://localhost:11434"
var coreWorkflow: String = """
1. **PLAN**: Analyze request.
2. **EXECUTE**: Return the JSON with commands and edits.
3. **VERIFY**: Check if the goal is achieved. Only run verification commands (test/build) if necessary to validate code changes. Do NOT verify simple info queries (e.g. pwd, ls).
""".trimIndent()

init {
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)
loadStancesFromResources()
}

private fun loadStancesFromResources() {
if (stances.isNotEmpty()) return

try {
val url = RoninSettingsState::class.java.getResource("/stances") ?: return

if (url.protocol == "file") {
val dir = java.io.File(url.toURI())
val files = dir.listFiles { _, name -> name.endsWith(".md") } ?: return

this.settingsEditable = profile.settingsEditable
for (file in files) {
val content = file.readText()
val stance = parseStanceMarkdown(file.nameWithoutExtension, content)
if (stance != null) {
stances.add(stance)
}
}
} else if (url.protocol == "jar") {
val connection = url.openConnection() as java.net.JarURLConnection
val jarFile = connection.jarFile
val entries = jarFile.entries()

if (profile.stances.isNotEmpty()) {
stances.addAll(profile.stances)
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val name = entry.name
// Check if it is within our target directory and is a markdown file
if (name.startsWith("stances/") && name.endsWith(".md") && !entry.isDirectory) {
val filename = name.substringAfterLast('/')
val id = filename.substringBeforeLast('.')

val inputStream = jarFile.getInputStream(entry)
val content = inputStream.bufferedReader().use { it.readText() }
val stance = parseStanceMarkdown(id, content)
if (stance != null) {
stances.add(stance)
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}

private fun parseStanceMarkdown(filenameId: String, content: String): Stance? {
try {
val parts = content.split("---", limit = 3)
if (parts.size < 3) return null // Invalid format

val frontmatter = parts[1]
val systemPrompt = parts[2].trim()

val stance = Stance(id = filenameId)
stance.systemPrompt = systemPrompt

// Parse Frontmatter (Simple Key-Value)
frontmatter.lines().forEach { line ->
val trimmed = line.trim()
if (trimmed.isNotBlank() && trimmed.contains(":")) {
val split = trimmed.split(":", limit = 2)
val key = split[0].trim()
val value = split[1].trim()

when (key) {
"name" -> stance.name = value
"description" -> stance.description = value
"provider" -> stance.provider = value
"model" -> stance.model = value
"credentialId" -> stance.credentialId = value
"encryptedKey" -> stance.encryptedKey = value
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return stance
} catch (e: Exception) {
println("Ronin: Failed to parse stance file $filenameId: ${e.message}")
return null
}
}

Expand All @@ -68,8 +126,6 @@
override fun loadState(state: RoninSettingsState) {
this.ollamaBaseUrl = state.ollamaBaseUrl
// allowedTools moved to Stance
this.coreWorkflow = state.coreWorkflow

this.activeStance = state.activeStance
this.settingsEditable = state.settingsEditable

Expand Down
Loading
Loading