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.