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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

class OpenAiCompatibleProvider(
private val http: HttpClient,
Expand All @@ -29,6 +30,9 @@ class OpenAiCompatibleProvider(
temperature = 0.1
)

val curlCmd = buildCurlCommand(endpoint, apiKey, req)
LlmRuntimeLogger.info("curl | $curlCmd")

val response = http.post(endpoint) {
header(HttpHeaders.Authorization, "Bearer $apiKey")
contentType(ContentType.Application.Json)
Expand Down Expand Up @@ -72,4 +76,14 @@ class OpenAiCompatibleProvider(
@Serializable
data class Choice(val message: Message)
}

companion object {
private val curlJson = Json { prettyPrint = false }

private fun buildCurlCommand(endpoint: String, apiKey: String, req: ChatCompletionsRequest): String {
val body = curlJson.encodeToString(ChatCompletionsRequest.serializer(), req)
.replace("'", "'\"'\"'")
return "curl -X POST '$endpoint' -H 'Authorization: Bearer ***' -H 'Content-Type: application/json' --data '$body'"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,17 @@ class CodeGenerateReviewAction : AnAction("Code Generate & Review"), DumbAware {
override fun run(indicator: ProgressIndicator) {
try {
indicator.text = "Calling LLM..."
val provider = LlmProviderFactory.create(LlmSettingsLoader.load(project))
val finalPrompt = AiPromptDefaults.render(
selectedPrompt.template,
mapOf(
"selectedCode" to selectedCode,
"extraRequirements" to extraRequirements
)
)
val response = runBlocking { provider.generateCode(finalPrompt) }
val response = LlmAuthSessionService.getInstance(project).withReloginOnUnauthorized { settings ->
val provider = LlmProviderFactory.create(settings)
runBlocking { provider.generateCode(finalPrompt) }
}
ApplicationManager.getApplication().invokeLater {
ContextBoxStateService.getInstance(project).recordCodePromptResult(
promptType = selectedPrompt.category.label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,37 @@ object HttpClients {
builder.sslSocketFactory(sslContext.socketFactory, trustAll[0] as X509TrustManager)
builder.hostnameVerifier { _, _ -> true }
}

fun logCurl(method: String, url: String, headers: Map<String, String>, body: String = "") {
val safeHeaders = headers.mapValues { (name, value) ->
if (name.contains("authorization", true) ||
name.contains("token", true) ||
name.contains("key", true) ||
name.contains("secret", true) ||
name.contains("password", true) ||
name.contains("cookie", true)
) "***" else value
}
val safeBody = redactSensitivePayload(body)
val headerArgs = safeHeaders.entries.joinToString(" ") { (name, value) ->
"-H \"$name: $value\""
}
val bodyArg = if (safeBody.isNotBlank()) " --data '${safeBody.replace("'", "'\"'\"'")}'" else ""
org.openprojectx.ai.plugin.llm.LlmRuntimeLogger.info(
"curl -X $method '$url'$headerArgs$bodyArg"
)
}

private fun redactSensitivePayload(text: String): String {
if (text.isBlank()) return text
val sensitiveKeys = listOf("password", "token", "access_token", "id_token", "refresh_token", "apiKey", "api_key", "secret")
var result = text
sensitiveKeys.forEach { key ->
val quotedJsonPattern = Regex("""("${Regex.escape(key)}"\s*:\s*")[^"]*(")""", RegexOption.IGNORE_CASE)
result = result.replace(quotedJsonPattern) { "${it.groupValues[1]}***${it.groupValues[2]}" }
val formPattern = Regex("""(?i)(^|[&\s])(${Regex.escape(key)}=)[^&\s]+""")
result = result.replace(formPattern) { "${it.groupValues[1]}${it.groupValues[2]}***" }
}
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,18 @@ object SonarCubeToolWindowPanel {

val result = runBlocking { SonarCubeToolWindowClient(config).load() }
ApplicationManager.getApplication().invokeLater {
refreshDashboard(dashboardPanel, bgColor, borderColor, commonFont, result, { filterByType(it) }, { resetTypeFilters() })
refreshDashboard(dashboardPanel, bgColor, borderColor, commonFont, result, { filterByType(it) }, { resetTypeFilters() }) {
val action = com.intellij.openapi.actionSystem.ActionManager.getInstance()
.getAction("org.openprojectx.ai.plugin.SonarQubeCoverageAction")
action?.let {
val event = com.intellij.openapi.actionSystem.AnActionEvent.createFromDataContext(
"SonarCubeToolWindow", null
) { dataId ->
if (com.intellij.openapi.actionSystem.CommonDataKeys.PROJECT.`is`(dataId)) project else null
}
it.actionPerformed(event)
}
}
reportDate.text = result.reportTimestamp ?: ""
allIssues.clear()
allIssues.addAll(result.issues)
Expand Down Expand Up @@ -329,8 +340,10 @@ object SonarCubeToolWindowPanel {
"sourceCode" to sourceCode
)
)
val provider = LlmProviderFactory.create(LlmSettingsLoader.load(project))
val response = runBlocking { provider.generateCode(prompt) }
val response = LlmAuthSessionService.getInstance(project).withReloginOnUnauthorized { settings ->
val provider = LlmProviderFactory.create(settings)
runBlocking { provider.generateCode(prompt) }
}

ApplicationManager.getApplication().invokeLater {
handleFixResponse(project, issue, response, sourceCode)
Expand Down Expand Up @@ -441,7 +454,8 @@ object SonarCubeToolWindowPanel {
font: Font,
result: Any,
onFilterByType: ((String) -> Unit)? = null,
onResetFilters: (() -> Unit)? = null
onResetFilters: (() -> Unit)? = null,
onCoverageClick: (() -> Unit)? = null
) {
container.removeAll()
container.background = bgColor
Expand All @@ -467,7 +481,7 @@ object SonarCubeToolWindowPanel {
isOpaque = false
}

cards.add(metricCard("Coverage", r.coverage?.formatPercent() ?: "—", "#42A5F5", r.coverage != null, valueFont, cardFont, borderColor))
cards.add(metricCard("Coverage", r.coverage?.formatPercent() ?: "—", "#42A5F5", r.coverage != null, valueFont, cardFont, borderColor, onCoverageClick))
cards.add(metricCard("Line Cov.", r.lineCoverage?.formatPercent() ?: "—", "#26C6DA", r.lineCoverage != null, valueFont, cardFont, borderColor))
cards.add(metricCard("Branch Cov.", r.branchCoverage?.formatPercent() ?: "—", "#009688", r.branchCoverage != null, valueFont, cardFont, borderColor))
cards.add(metricCard("Uncovered", r.uncoveredLines?.toString() ?: "—", "#EF5350", r.uncoveredLines != null, valueFont, cardFont, borderColor))
Expand Down Expand Up @@ -757,14 +771,14 @@ private class SonarCubeToolWindowClient(private val config: SonarQubeConfig) {
try {
val baseUrl = config.serverUrl.trimEnd('/')
val projectKey = encoded(config.projectKey)
val measures: SonarCubeMeasuresResponse = client.get(
"$baseUrl/api/measures/component?component=$projectKey&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines,bugs,vulnerabilities,code_smells"
) {
val measuresUrl = "$baseUrl/api/measures/component?component=$projectKey&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines,bugs,vulnerabilities,code_smells"
HttpClients.logCurl("GET", measuresUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap())
val measures: SonarCubeMeasuresResponse = client.get(measuresUrl) {
authHeader?.let { header(HttpHeaders.Authorization, it) }
}.body()
val issues: SonarCubeIssuesResponse = client.get(
"$baseUrl/api/issues/search?componentKeys=$projectKey&resolved=false&ps=100&s=SEVERITY&asc=false"
) {
val issuesUrl = "$baseUrl/api/issues/search?componentKeys=$projectKey&resolved=false&ps=100&s=SEVERITY&asc=false"
HttpClients.logCurl("GET", issuesUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap())
val issues: SonarCubeIssuesResponse = client.get(issuesUrl) {
authHeader?.let { header(HttpHeaders.Authorization, it) }
}.body()
val now = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ class SonarQubeCoverageAction : AnAction("SonarQube Coverage"), DumbAware {
val generation = if (request.generateMissingTests) {
indicator.text = "Generating tests for uncovered code..."
val prompt = SonarQubeCoveragePromptBuilder.build(project, report, request.targetCoverage)
val provider = LlmProviderFactory.create(LlmSettingsLoader.load(project))
runBlocking { provider.generateCode(prompt) }
LlmAuthSessionService.getInstance(project).withReloginOnUnauthorized { settings ->
val provider = LlmProviderFactory.create(settings)
runBlocking { provider.generateCode(prompt) }
}
} else {
""
}
Expand Down Expand Up @@ -184,14 +186,14 @@ private class SonarQubeCoverageClient(private val request: SonarQubeCoverageRequ
try {
val baseUrl = request.serverUrl.trimEnd('/')
val component = encoded(request.projectKey)
val projectMeasures: SonarComponentMeasuresResponse = jsonClient.get(
"$baseUrl/api/measures/component?component=$component&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines"
) {
val projectMeasuresUrl = "$baseUrl/api/measures/component?component=$component&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines"
HttpClients.logCurl("GET", projectMeasuresUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap())
val projectMeasures: SonarComponentMeasuresResponse = jsonClient.get(projectMeasuresUrl) {
authHeader?.let { header(HttpHeaders.Authorization, it) }
}.body()
val fileMeasures: SonarComponentTreeResponse = jsonClient.get(
"$baseUrl/api/measures/component_tree?component=$component&metricKeys=coverage,uncovered_lines&qualifiers=FIL&s=metric&metricSort=uncovered_lines&asc=false&ps=${request.maxFiles.coerceIn(1, 100)}"
) {
val fileMeasuresUrl = "$baseUrl/api/measures/component_tree?component=$component&metricKeys=coverage,uncovered_lines&qualifiers=FIL&s=metric&metricSort=uncovered_lines&asc=false&ps=${request.maxFiles.coerceIn(1, 100)}"
HttpClients.logCurl("GET", fileMeasuresUrl, authHeader?.let { mapOf("Authorization" to it) } ?: emptyMap())
val fileMeasures: SonarComponentTreeResponse = jsonClient.get(fileMeasuresUrl) {
authHeader?.let { header(HttpHeaders.Authorization, it) }
}.body()

Expand Down
Loading