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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
.kotlin
.qodana
build
src/main/resources/ronin-profile.json
18 changes: 9 additions & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"))

Expand Down Expand Up @@ -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")
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions src/main/kotlin/com/ronin/actions/BaseRoninAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ abstract class BaseRoninAction : AnAction() {
// 3. Call Service
com.intellij.openapi.application.ApplicationManager.getApplication().executeOnPooledThread {
val llmService = project.service<LLMService>()
val configService = project.service<com.ronin.service.RoninConfigService>()

// Gather Context
val activeFile = configService.getActiveFileContent()
val projectStructure = configService.getProjectStructure()
val activeFile = com.intellij.openapi.application.ReadAction.compute<String?, Throwable> {
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())

Expand Down
18 changes: 8 additions & 10 deletions src/main/kotlin/com/ronin/service/EditService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@
}
}

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()) {

Check warning on line 48 in src/main/kotlin/com/ronin/service/EditService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Usage of redundant or deprecated syntax or deprecated symbols

Unnecessary non-null assertion (!!) on a non-null receiver of type VirtualFile
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)
Expand Down Expand Up @@ -136,9 +135,8 @@
// 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}")
Expand Down
50 changes: 27 additions & 23 deletions src/main/kotlin/com/ronin/service/LLMService.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.ronin.service

import com.intellij.openapi.components.service

Check warning on line 3 in src/main/kotlin/com/ronin/service/LLMService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import com.intellij.openapi.project.Project

interface LLMService {
Expand All @@ -9,32 +9,26 @@
fun fetchAvailableModels(provider: String): List<String>
}

class LLMServiceImpl(private val project: Project) : LLMService {

Check warning on line 12 in src/main/kotlin/com/ronin/service/LLMService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Property "project" is never used
private val client = java.net.http.HttpClient.newHttpClient()

override fun sendMessage(prompt: String, context: String?, history: List<Map<String, String>>, images: List<String>): 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<RoninConfigService>()
val projectContext = configService.getProjectStructure()
val projectRules = configService.getProjectRules()

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}
- $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:
Expand All @@ -57,6 +51,11 @@
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:**

Expand Down Expand Up @@ -113,9 +112,16 @@
""".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."

Expand Down Expand Up @@ -197,15 +203,12 @@

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)
}

Expand Down Expand Up @@ -256,17 +259,18 @@

internal fun parseModelsJson(json: String): List<String> {
return try {
val tempMap = com.google.gson.Gson().fromJson(json, Map::class.java) as Map<String, Any>
val data = tempMap["data"] as? List<Map<String, Any>> ?: emptyList()
val tempMap = com.google.gson.Gson().fromJson(json, Map::class.java) as Map<*, *>
val data = tempMap["data"] as? List<*> ?: emptyList<Any>()

val models = mutableListOf<String>()

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<String, Any>)
?: (modelObj["features"] as? List<String>)?.associate { it to true }
?: emptyMap()
val capabilities = (modelObj["capabilities"] as? Map<*, *>)
?: (modelObj["features"] as? List<*>)?.associate { it.toString() to true }
?: emptyMap<Any, Any>()

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
Expand Down
38 changes: 0 additions & 38 deletions src/main/kotlin/com/ronin/service/RoninConfigService.kt

This file was deleted.

36 changes: 28 additions & 8 deletions src/main/kotlin/com/ronin/service/TerminalService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
}
Expand Down
Loading
Loading