diff --git a/README.md b/README.md
index 9dd8100..88182aa 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ plugin and configure a LLM API client in plugin's settings: Settings
- Amazon Bedrock
- Anthropic
- Azure Open AI
+- Claude Code (via CLI)
- Gemini Google AI
- Gemini Vertex AI
- GitHub Models
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt
index 12e2313..6be1df0 100644
--- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt
@@ -24,6 +24,7 @@ object Icons {
val GEMINI_VERTEX = AICommitsIcon("/icons/geminiVertex.svg", null)
val GEMINI_GOOGLE = AICommitsIcon("/icons/geminiGoogle.svg", null)
val ANTHROPIC = AICommitsIcon("/icons/anthropic15bright.svg", "/icons/anthropic15dark.svg")
+ val CLAUDE_CODE = AICommitsIcon("/icons/claudeCode15.svg", null)
val AZURE_OPEN_AI = AICommitsIcon("/icons/azureOpenAi.svg", null)
val HUGGING_FACE = AICommitsIcon("/icons/huggingface.svg", null)
val GITHUB = AICommitsIcon("/icons/github15bright.svg", "/icons/github15dark.svg")
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt
index dfedd37..9e5a027 100644
--- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt
@@ -8,6 +8,7 @@ import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientCon
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.amazonBedrock.AmazonBedrockClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.anthropic.AnthropicClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.azureOpenAi.AzureOpenAiClientConfiguration
+import com.github.blarc.ai.commits.intellij.plugin.settings.clients.claudeCode.ClaudeCodeClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiGoogle.GeminiGoogleClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiVertex.GeminiClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.githubModels.GitHubModelsClientConfiguration
@@ -68,7 +69,8 @@ class AppSettings2 : PersistentStateComponent {
HuggingFaceClientConfiguration::class,
GitHubModelsClientConfiguration::class,
MistralAIClientConfiguration::class,
- AmazonBedrockClientConfiguration::class
+ AmazonBedrockClientConfiguration::class,
+ ClaudeCodeClientConfiguration::class
],
style = XCollection.Style.v2
)
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientConfiguration.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientConfiguration.kt
index e1de155..651b40a 100644
--- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientConfiguration.kt
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientConfiguration.kt
@@ -3,6 +3,7 @@ package com.github.blarc.ai.commits.intellij.plugin.settings.clients
import com.github.blarc.ai.commits.intellij.plugin.Icons
import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification
import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification
+import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings2
import com.github.blarc.ai.commits.intellij.plugin.settings.ProjectSettings
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
@@ -66,7 +67,10 @@ abstract class LLMClientConfiguration(
val projectSettings = project.service()
projectSettings.splitButtonActionSelectedLLMClientId = this.id
- generateCommitMessage(commitWorkflowHandler, project)
+ // Look up the current configuration by ID to ensure we use the latest settings
+ // (AnAction instances may be cached by IntelliJ and hold stale values)
+ val currentConfig = AppSettings2.instance.llmClientConfigurations.find { it.id == this.id } ?: this
+ currentConfig.generateCommitMessage(commitWorkflowHandler, project)
}
open fun setCommitMessage(commitWorkflowHandler: AbstractCommitWorkflowHandler<*, *>, prompt: String, result: String) {
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt
index a1e09ee..4bab0ef 100644
--- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt
@@ -5,6 +5,7 @@ import com.github.blarc.ai.commits.intellij.plugin.createColumn
import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings2
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.amazonBedrock.AmazonBedrockClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.anthropic.AnthropicClientConfiguration
+import com.github.blarc.ai.commits.intellij.plugin.settings.clients.claudeCode.ClaudeCodeClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.azureOpenAi.AzureOpenAiClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiGoogle.GeminiGoogleClientConfiguration
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiVertex.GeminiClientConfiguration
@@ -120,6 +121,7 @@ class LLMClientTable {
var llmClient = newLlmClientConfiguration ?: llmClientConfigurations[0]
private val cardLayout = JBCardLayout()
+ private var editPanel: DialogPanel? = null
init {
title = newLlmClientConfiguration?.let { "Edit LLM Client" } ?: "Add LLM Client"
@@ -130,15 +132,17 @@ class LLMClientTable {
override fun doOKAction() {
if (newLlmClientConfiguration == null) {
(cardLayout.findComponentById(llmClient.getClientName()) as DialogPanel).apply()
+ } else {
+ // Apply the edit panel to save typed values from editable comboboxes
+ editPanel?.apply()
}
- // TODO: Figure out how to call apply of the currently active panel
super.doOKAction()
}
override fun createCenterPanel() = if (newLlmClientConfiguration == null) {
createCardSplitter()
} else {
- llmClient.panel().create()
+ llmClient.panel().create().also { editPanel = it }
}.apply {
isResizable = false
// Add 200 so there is space for verification message.
@@ -159,7 +163,8 @@ class LLMClientTable {
HuggingFaceClientConfiguration(),
GitHubModelsClientConfiguration(),
MistralAIClientConfiguration(),
- AmazonBedrockClientConfiguration()
+ AmazonBedrockClientConfiguration(),
+ ClaudeCodeClientConfiguration()
).sortedBy { it.getClientName() }
} else {
listOf(newLLMClientConfiguration)
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientConfiguration.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientConfiguration.kt
new file mode 100644
index 0000000..7b86ac4
--- /dev/null
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientConfiguration.kt
@@ -0,0 +1,60 @@
+package com.github.blarc.ai.commits.intellij.plugin.settings.clients.claudeCode
+
+import com.github.blarc.ai.commits.intellij.plugin.Icons
+import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientConfiguration
+import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientSharedState
+import com.intellij.openapi.project.Project
+import com.intellij.util.xmlb.annotations.Attribute
+import com.intellij.vcs.commit.AbstractCommitWorkflowHandler
+import kotlinx.coroutines.Job
+import javax.swing.Icon
+
+class ClaudeCodeClientConfiguration : LLMClientConfiguration(
+ "Claude Code",
+ "", // No default model - uses CLI's configured model
+ "" // No temperature - CLI doesn't support it
+) {
+
+ @Attribute
+ var cliPath: String = "" // Empty means auto-detect
+
+ @Attribute
+ var timeout: Int = 120 // Longer default for CLI execution
+
+ companion object {
+ const val CLIENT_NAME = "Claude Code"
+ }
+
+ override fun getClientName(): String {
+ return CLIENT_NAME
+ }
+
+ override fun getClientIcon(): Icon {
+ return Icons.CLAUDE_CODE.getThemeBasedIcon()
+ }
+
+ override fun getSharedState(): LLMClientSharedState {
+ return ClaudeCodeClientSharedState.getInstance()
+ }
+
+ override fun generateCommitMessage(commitWorkflowHandler: AbstractCommitWorkflowHandler<*, *>, project: Project) {
+ return ClaudeCodeClientService.getInstance().generateCommitMessageCli(this, commitWorkflowHandler, project)
+ }
+
+ override fun getGenerateCommitMessageJob(): Job? {
+ return ClaudeCodeClientService.getInstance().generateCommitMessageJob
+ }
+
+ override fun clone(): LLMClientConfiguration {
+ val copy = ClaudeCodeClientConfiguration()
+ copy.id = id
+ copy.name = name
+ copy.cliPath = cliPath
+ copy.timeout = timeout
+ copy.modelId = modelId
+ copy.temperature = temperature
+ return copy
+ }
+
+ override fun panel() = ClaudeCodeClientPanel(this)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientPanel.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientPanel.kt
new file mode 100644
index 0000000..5a7e618
--- /dev/null
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientPanel.kt
@@ -0,0 +1,95 @@
+package com.github.blarc.ai.commits.intellij.plugin.settings.clients.claudeCode
+
+import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message
+import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientPanel
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
+import com.intellij.ui.components.JBTextField
+import com.intellij.ui.dsl.builder.*
+
+class ClaudeCodeClientPanel private constructor(
+ private val clientConfiguration: ClaudeCodeClientConfiguration,
+ val service: ClaudeCodeClientService
+) : LLMClientPanel(clientConfiguration) {
+
+ private val cliPathTextField = JBTextField()
+ private val timeoutTextField = JBTextField()
+
+ constructor(configuration: ClaudeCodeClientConfiguration) : this(configuration, ClaudeCodeClientService.getInstance())
+
+ override fun create() = panel {
+ nameRow()
+ cliPathRow()
+ timeoutRow()
+ modelRow()
+ verifyRow()
+ }
+
+ private fun Panel.cliPathRow() {
+ row {
+ label(message("settings.claudeCode.cliPath"))
+ .widthGroup("label")
+ cell(cliPathTextField)
+ .bindText(clientConfiguration::cliPath)
+ .align(Align.FILL)
+ .resizableColumn()
+ .comment(message("settings.claudeCode.cliPath.comment"))
+ button(message("settings.claudeCode.browse")) {
+ val descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor()
+ val chooser = com.intellij.openapi.fileChooser.FileChooser.chooseFile(descriptor, null, null)
+ chooser?.let {
+ cliPathTextField.text = it.path
+ }
+ }.widthGroup("button")
+ button(message("settings.claudeCode.detectPath")) {
+ val detectedPath = service.findClaudePath("")
+ if (detectedPath != null) {
+ cliPathTextField.text = detectedPath
+ }
+ }.widthGroup("button")
+ }
+ }
+
+ private fun Panel.timeoutRow() {
+ row {
+ label(message("settings.llmClient.timeout"))
+ .widthGroup("label")
+ cell(timeoutTextField)
+ .bindIntText(clientConfiguration::timeout)
+ .resizableColumn()
+ .align(Align.FILL)
+ .comment(message("settings.claudeCode.timeout.comment"))
+ }
+ }
+
+ private fun Panel.modelRow() {
+ row {
+ label(message("settings.claudeCode.model"))
+ .widthGroup("label")
+ cell(modelComboBox)
+ .applyToComponent {
+ isEditable = true
+ }
+ .bindItem({ clientConfiguration.modelId }, {
+ if (it != null) {
+ clientConfiguration.modelId = it
+ }
+ })
+ .onApply {
+ // Explicitly capture typed value from editable combobox
+ // bindItem doesn't reliably capture typed values not in the dropdown
+ modelComboBox.item?.let { clientConfiguration.modelId = it }
+ }
+ .align(Align.FILL)
+ .resizableColumn()
+ .comment(message("settings.claudeCode.model.comment"))
+ }
+ }
+
+ override fun verifyConfiguration() {
+ clientConfiguration.cliPath = cliPathTextField.text
+ clientConfiguration.timeout = timeoutTextField.text.toIntOrNull() ?: 120
+ clientConfiguration.modelId = modelComboBox.item ?: ""
+
+ service.verifyConfigurationCli(clientConfiguration, verifyLabel)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientService.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientService.kt
new file mode 100644
index 0000000..e03824e
--- /dev/null
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientService.kt
@@ -0,0 +1,312 @@
+package com.github.blarc.ai.commits.intellij.plugin.settings.clients.claudeCode
+
+import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message
+import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.computeDiff
+import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.constructPrompt
+import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.getCommonBranch
+import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification
+import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification
+import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings2
+import com.github.blarc.ai.commits.intellij.plugin.settings.ProjectSettings
+import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientService
+import com.github.blarc.ai.commits.intellij.plugin.wrap
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.application.asContextElement
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.platform.ide.progress.withBackgroundProgress
+import com.intellij.ui.components.JBLabel
+import com.intellij.vcs.commit.AbstractCommitWorkflowHandler
+import com.intellij.vcs.commit.isAmendCommitMode
+import dev.langchain4j.model.chat.ChatModel
+import dev.langchain4j.model.chat.StreamingChatModel
+import git4idea.GitCommit
+import git4idea.history.GitHistoryUtils
+import git4idea.repo.GitRepositoryManager
+import kotlinx.coroutines.*
+import kotlinx.serialization.json.*
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+@Service(Service.Level.APP)
+class ClaudeCodeClientService(private val cs: CoroutineScope) : LLMClientService(cs) {
+
+ companion object {
+ @JvmStatic
+ fun getInstance(): ClaudeCodeClientService = service()
+
+ private val HOME = System.getProperty("user.home")
+
+ private val CLAUDE_PATHS = listOfNotNull(
+ // Common npm global locations
+ "$HOME/.npm-global/bin/claude",
+ "$HOME/.npm/bin/claude",
+ "/usr/local/bin/claude",
+ "/usr/bin/claude",
+ // nvm installations
+ "$HOME/.nvm/versions/node/*/bin/claude",
+ // Claude-specific locations
+ "$HOME/.claude/local/claude",
+ "$HOME/.local/bin/claude",
+ // Windows
+ System.getenv("LOCALAPPDATA")?.let { "$it\\Claude\\claude.exe" },
+ System.getenv("APPDATA")?.let { "$it\\npm\\claude.cmd" }
+ )
+ }
+
+ override suspend fun buildChatModel(client: ClaudeCodeClientConfiguration): ChatModel {
+ throw UnsupportedOperationException("Claude Code uses CLI invocation, not langchain4j ChatModel")
+ }
+
+ override suspend fun buildStreamingChatModel(client: ClaudeCodeClientConfiguration): StreamingChatModel? {
+ return null // CLI-based, no streaming model
+ }
+
+ fun findClaudePath(configuredPath: String): String? {
+ // Use configured path if provided
+ if (configuredPath.isNotBlank()) {
+ val file = File(configuredPath)
+ if (file.exists() && file.canExecute()) {
+ return configuredPath
+ }
+ return null
+ }
+
+ // Try to find claude using shell (inherits user's PATH)
+ try {
+ val isWindows = System.getProperty("os.name").lowercase().contains("win")
+ val shellCommand = if (isWindows) {
+ arrayOf("cmd", "/c", "where claude")
+ } else {
+ // Use interactive shell to get user's PATH from .bashrc
+ arrayOf("/bin/bash", "-i", "-c", "which claude 2>/dev/null")
+ }
+ val process = ProcessBuilder(*shellCommand)
+ .start() // Don't redirect stderr - we only want stdout
+ process.outputStream.close() // Close stdin
+ val completed = process.waitFor(5, TimeUnit.SECONDS)
+ val exitValue = if (completed) process.exitValue() else -1
+ if (completed && exitValue == 0) {
+ // Read all lines and find one that looks like a path (interactive bash may print warnings)
+ val lines = process.inputStream.bufferedReader().readLines()
+ val result = lines.firstOrNull { it.trim().startsWith("/") }?.trim()
+ if (!result.isNullOrBlank()) {
+ val file = File(result)
+ if (file.exists()) {
+ // Resolve symlinks to get the real path
+ return try {
+ file.toPath().toRealPath().toString()
+ } catch (e: Exception) {
+ result
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ // Fall through to hardcoded paths
+ }
+
+ // Try hardcoded common locations
+ for (pathPattern in CLAUDE_PATHS) {
+ try {
+ if (pathPattern.contains("*")) {
+ // Handle glob patterns (e.g., nvm paths)
+ val parts = pathPattern.split("*")
+ if (parts.size == 2) {
+ val parentDir = File(parts[0]).parentFile
+ if (parentDir?.exists() == true) {
+ parentDir.listFiles()?.forEach { dir ->
+ val candidate = File(dir, parts[1].removePrefix("/"))
+ if (candidate.exists() && candidate.canExecute()) {
+ return candidate.absolutePath
+ }
+ }
+ }
+ }
+ } else {
+ val file = File(pathPattern)
+ if (file.exists() && file.canExecute()) {
+ return pathPattern
+ }
+ }
+ } catch (e: Exception) {
+ // Continue to next path
+ }
+ }
+ return null
+ }
+
+ private suspend fun executeClaudeCli(
+ client: ClaudeCodeClientConfiguration,
+ prompt: String
+ ): Result = withContext(Dispatchers.IO) {
+ val claudePath = findClaudePath(client.cliPath)
+ ?: return@withContext Result.failure(
+ IllegalStateException(message("claudeCode.cliNotFound"))
+ )
+
+ val command = mutableListOf(claudePath, "-p", "--output-format", "json")
+
+ // Add model if specified
+ if (client.modelId.isNotBlank()) {
+ command.add("--model")
+ command.add(client.modelId)
+ }
+
+ // Add the prompt as the last argument
+ command.add(prompt)
+
+ try {
+ val process = ProcessBuilder(command)
+ .redirectErrorStream(true)
+ .start()
+
+ // Close stdin immediately to prevent CLI from waiting for input
+ process.outputStream.close()
+
+ // Read output in a separate thread to prevent buffer deadlock
+ val outputFuture = java.util.concurrent.CompletableFuture.supplyAsync {
+ process.inputStream.bufferedReader().readText()
+ }
+
+ val completed = process.waitFor(client.timeout.toLong(), TimeUnit.SECONDS)
+ if (!completed) {
+ process.destroyForcibly()
+ outputFuture.cancel(true)
+ return@withContext Result.failure(
+ IllegalStateException(message("claudeCode.timeout"))
+ )
+ }
+
+ val output = try {
+ outputFuture.get(5, TimeUnit.SECONDS)
+ } catch (e: Exception) {
+ ""
+ }
+
+ if (process.exitValue() != 0) {
+ return@withContext Result.failure(
+ IllegalStateException("CLI exited with code ${process.exitValue()}: $output")
+ )
+ }
+
+ // Parse JSON response
+ parseClaudeResponse(output)
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
+ }
+
+ private fun parseClaudeResponse(jsonOutput: String): Result {
+ return try {
+ val json = Json { ignoreUnknownKeys = true }
+ val response = json.parseToJsonElement(jsonOutput).jsonObject
+
+ val isError = response["is_error"]?.jsonPrimitive?.booleanOrNull ?: false
+ val result = response["result"]?.jsonPrimitive?.contentOrNull
+
+ if (result == null) {
+ Result.failure(IllegalStateException("No result in Claude response"))
+ } else if (isError) {
+ Result.failure(IllegalStateException(result))
+ } else {
+ Result.success(result)
+ }
+ } catch (e: Exception) {
+ Result.failure(IllegalStateException("Failed to parse Claude response: ${e.message}"))
+ }
+ }
+
+ fun generateCommitMessageCli(
+ clientConfiguration: ClaudeCodeClientConfiguration,
+ commitWorkflowHandler: AbstractCommitWorkflowHandler<*, *>,
+ project: Project
+ ) {
+ val commitContext = commitWorkflowHandler.workflow.commitContext
+ val includedChanges = commitWorkflowHandler.ui.getIncludedChanges().toMutableList()
+
+ generateCommitMessageJob = cs.launch(ModalityState.current().asContextElement()) {
+ withBackgroundProgress(project, message("action.background")) {
+ if (commitContext.isAmendCommitMode) {
+ includedChanges += getLastCommitChanges(project)
+ }
+
+ val diff = computeDiff(includedChanges, false, project)
+ if (diff.isBlank()) {
+ withContext(Dispatchers.EDT) {
+ sendNotification(Notification.emptyDiff())
+ }
+ return@withBackgroundProgress
+ }
+
+ val branch = getCommonBranch(includedChanges, project)
+ val prompt = constructPrompt(
+ project.service().activePrompt.content,
+ diff,
+ branch,
+ commitWorkflowHandler.getCommitMessage(),
+ project
+ )
+
+ val result = executeClaudeCli(clientConfiguration, prompt)
+
+ result.fold(
+ onSuccess = { commitMessage ->
+ withContext(Dispatchers.EDT) {
+ commitWorkflowHandler.setCommitMessage(commitMessage)
+ }
+ AppSettings2.instance.recordHit()
+ },
+ onFailure = { error ->
+ withContext(Dispatchers.EDT) {
+ commitWorkflowHandler.setCommitMessage(error.message ?: message("unknown-error"))
+ }
+ }
+ )
+ }
+ }
+ }
+
+ fun verifyConfigurationCli(client: ClaudeCodeClientConfiguration, label: JBLabel) {
+ label.text = message("settings.verify.running")
+ label.icon = AllIcons.General.InlineRefresh
+ cs.launch(ModalityState.current().asContextElement()) {
+ val claudePath = findClaudePath(client.cliPath)
+ if (claudePath == null) {
+ withContext(Dispatchers.EDT) {
+ label.text = message("claudeCode.cliNotFound").wrap(60)
+ label.icon = AllIcons.General.InspectionsError
+ }
+ return@launch
+ }
+
+ // Test with a simple prompt
+ val result = executeClaudeCli(client, "Say 'OK' in exactly one word")
+ withContext(Dispatchers.EDT) {
+ result.fold(
+ onSuccess = {
+ label.text = message("settings.verify.valid")
+ label.icon = AllIcons.General.InspectionsOK
+ },
+ onFailure = { error ->
+ label.text = (error.message ?: message("unknown-error")).wrap(60)
+ label.icon = AllIcons.General.InspectionsError
+ }
+ )
+ }
+ }
+ }
+
+ private suspend fun getLastCommitChanges(project: Project) = withContext(Dispatchers.IO) {
+ GitRepositoryManager.getInstance(project).repositories.map { repo ->
+ GitHistoryUtils.history(project, repo.root, "--max-count=1")
+ }.filter { commits ->
+ commits.isNotEmpty()
+ }.map { commits ->
+ (commits.first() as GitCommit).changes
+ }.flatten()
+ }
+}
diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientSharedState.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientSharedState.kt
new file mode 100644
index 0000000..f2929ec
--- /dev/null
+++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/claudeCode/ClaudeCodeClientSharedState.kt
@@ -0,0 +1,35 @@
+package com.github.blarc.ai.commits.intellij.plugin.settings.clients.claudeCode
+
+import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientSharedState
+import com.intellij.openapi.components.*
+import com.intellij.util.xmlb.annotations.XCollection
+
+@Service(Service.Level.APP)
+@State(name = "ClaudeCodeClientSharedState", storages = [Storage("AICommitsClaudeCode.xml")])
+class ClaudeCodeClientSharedState : PersistentStateComponent, LLMClientSharedState {
+
+ companion object {
+ @JvmStatic
+ fun getInstance(): ClaudeCodeClientSharedState = service()
+ }
+
+ // Claude Code CLI doesn't have host configuration
+ @XCollection(style = XCollection.Style.v2)
+ override val hosts: MutableSet = mutableSetOf()
+
+ // Common model aliases for Claude Code CLI
+ @XCollection(style = XCollection.Style.v2)
+ override val modelIds: MutableSet = mutableSetOf(
+ "", // Empty = use CLI default
+ "sonnet",
+ "opus",
+ "haiku"
+ )
+
+ override fun getState(): ClaudeCodeClientSharedState = this
+
+ override fun loadState(state: ClaudeCodeClientSharedState) {
+ modelIds += state.modelIds
+ hosts += state.hosts
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/icons/claudeCode15.svg b/src/main/resources/icons/claudeCode15.svg
new file mode 100644
index 0000000..c65dd62
--- /dev/null
+++ b/src/main/resources/icons/claudeCode15.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/src/main/resources/messages/AiCommitsBundle.properties b/src/main/resources/messages/AiCommitsBundle.properties
index aa58b9e..091d6c3 100644
--- a/src/main/resources/messages/AiCommitsBundle.properties
+++ b/src/main/resources/messages/AiCommitsBundle.properties
@@ -161,3 +161,13 @@ settings.amazonBedrock.defaultCredentialsProvider=Default
settings.amazonBedrock.staticCredentialsProvider=Static
settings.amazonBedrock.profileName=Profile name
settings.amazonBedrock.defaultCredentialsProvider.comment=Use Default for discovering credentials from the host's environment and Static for a fixed set of credentials.
+
+settings.claudeCode.cliPath=CLI Path
+settings.claudeCode.cliPath.comment=Path to the Claude Code CLI executable. Leave empty to auto-detect from PATH.
+settings.claudeCode.browse=Browse...
+settings.claudeCode.detectPath=Detect
+settings.claudeCode.timeout.comment=Timeout in seconds for CLI execution.
+settings.claudeCode.model=Model
+settings.claudeCode.model.comment=Model alias (sonnet, opus, haiku) or full name. Leave empty to use CLI default.
+claudeCode.cliNotFound=Claude Code CLI not found. Please install it from https://claude.ai/download or specify the path manually.
+claudeCode.timeout=Claude Code CLI execution timed out.