Skip to content

Commit 34b04ae

Browse files
authored
Implement AI commit message generation for Jetbrains (#2442)
* feat(jetbrains): add AI-powered commit message generation action Add checkin handler factory and application service for commit message handling, along with a new action in the VCS message group to generate AI-powered commit messages.
1 parent 44b84fe commit 34b04ae

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2863
-1024
lines changed

.changeset/slow-words-stay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kilo-code": minor
3+
---
4+
5+
Added AI powered commit message generation to Jetbrains IDEs
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
// kilocode_change - new file
6+
package ai.kilocode.jetbrains.actions
7+
8+
import ai.kilocode.jetbrains.git.CommitMessageService
9+
import ai.kilocode.jetbrains.git.WorkspaceResolver
10+
import ai.kilocode.jetbrains.git.FileDiscoveryService
11+
import ai.kilocode.jetbrains.i18n.I18n
12+
import com.intellij.openapi.actionSystem.ActionUpdateThread
13+
import com.intellij.openapi.actionSystem.AnAction
14+
import com.intellij.openapi.actionSystem.AnActionEvent
15+
import com.intellij.openapi.actionSystem.DataContext
16+
import com.intellij.openapi.application.ApplicationManager
17+
import com.intellij.openapi.diagnostic.Logger
18+
import com.intellij.openapi.progress.ProgressIndicator
19+
import com.intellij.openapi.progress.ProgressManager
20+
import com.intellij.openapi.progress.Task
21+
import com.intellij.openapi.project.Project
22+
import com.intellij.openapi.ui.Messages
23+
import com.intellij.openapi.vcs.VcsDataKeys
24+
import com.intellij.openapi.vcs.changes.ChangeListManager
25+
import com.intellij.vcs.commit.CommitMessageUi
26+
import kotlinx.coroutines.runBlocking
27+
28+
class GitCommitMessageAction : AnAction(I18n.t("kilocode:commitMessage.ui.generateButton")) {
29+
private val logger: Logger = Logger.getInstance(GitCommitMessageAction::class.java)
30+
private val commitMessageService = CommitMessageService.getInstance()
31+
private val fileDiscoveryService = FileDiscoveryService()
32+
33+
override fun getActionUpdateThread(): ActionUpdateThread {
34+
return ActionUpdateThread.BGT
35+
}
36+
37+
override fun update(e: AnActionEvent) {
38+
val project = e.project
39+
val presentation = e.presentation
40+
41+
if (project == null) {
42+
presentation.isEnabled = false
43+
presentation.description = I18n.t("kilocode:commitMessage.errors.noProject")
44+
return
45+
}
46+
47+
val changeListManager = ChangeListManager.getInstance(project)
48+
val hasAnyChanges = changeListManager.allChanges.isNotEmpty()
49+
50+
presentation.isEnabled = hasAnyChanges
51+
presentation.description = if (hasAnyChanges) {
52+
I18n.t("kilocode:commitMessage.ui.generateButtonTooltip")
53+
} else {
54+
I18n.t("kilocode:commitMessage.errors.noChanges")
55+
}
56+
}
57+
58+
override fun actionPerformed(e: AnActionEvent) {
59+
val project = e.project
60+
if (project == null) {
61+
logger.warn("No project available for commit message generation")
62+
return
63+
}
64+
65+
val workspacePath = WorkspaceResolver.getWorkspacePathOrShowError(
66+
project,
67+
I18n.t("kilocode:commitMessage.errors.noWorkspacePath"),
68+
I18n.t("kilocode:commitMessage.dialogs.error"),
69+
) ?: return
70+
71+
val dataContext = e.dataContext
72+
val commitControl = VcsDataKeys.COMMIT_MESSAGE_CONTROL.getData(dataContext)
73+
74+
when {
75+
commitControl is CommitMessageUi -> {
76+
generateAndSetCommitMessage(project, commitControl, workspacePath, dataContext)
77+
}
78+
else -> {
79+
generateAndOpenCommitDialog(project, workspacePath, dataContext)
80+
}
81+
}
82+
}
83+
84+
private fun generateAndSetCommitMessage(
85+
project: Project,
86+
commitControl: CommitMessageUi,
87+
workspacePath: String,
88+
dataContext: DataContext
89+
) {
90+
ProgressManager.getInstance().run(object : Task.Backgroundable(
91+
project,
92+
I18n.t("kilocode:commitMessage.progress.title"),
93+
true,
94+
) {
95+
override fun run(indicator: ProgressIndicator) {
96+
indicator.text = I18n.t("kilocode:commitMessage.progress.analyzing")
97+
indicator.isIndeterminate = true
98+
99+
try {
100+
val discoveryResult = fileDiscoveryService.discoverFilesWithResult(project, dataContext)
101+
val files = when (discoveryResult) {
102+
is FileDiscoveryService.FileDiscoveryResult.Success -> discoveryResult.files
103+
is FileDiscoveryService.FileDiscoveryResult.Error -> emptyList()
104+
FileDiscoveryService.FileDiscoveryResult.NoFiles -> emptyList()
105+
}
106+
107+
indicator.text = I18n.t("kilocode:commitMessage.progress.generating")
108+
val result = runBlocking {
109+
commitMessageService.generateCommitMessage(project, workspacePath, files.ifEmpty { null })
110+
}
111+
112+
ApplicationManager.getApplication().invokeLater {
113+
when (result) {
114+
is CommitMessageService.Result.Success -> {
115+
commitControl.text = result.message
116+
}
117+
is CommitMessageService.Result.Error -> {
118+
Messages.showErrorDialog(
119+
project,
120+
result.errorMessage,
121+
I18n.t("kilocode:commitMessage.dialogs.error"),
122+
)
123+
}
124+
}
125+
}
126+
} catch (e: Exception) {
127+
logger.error("Error generating commit message", e)
128+
ApplicationManager.getApplication().invokeLater {
129+
Messages.showErrorDialog(
130+
project,
131+
I18n.t("kilocode:commitMessage.errors.processingError",
132+
mapOf("error" to (e.message ?: I18n.t("kilocode:commitMessage.error.unknown")))),
133+
I18n.t("kilocode:commitMessage.dialogs.error"),
134+
)
135+
}
136+
}
137+
}
138+
})
139+
}
140+
141+
private fun generateAndOpenCommitDialog(project: Project, workspacePath: String, dataContext: DataContext) {
142+
ProgressManager.getInstance().run(object : Task.Backgroundable(
143+
project,
144+
I18n.t("kilocode:commitMessage.progress.title"),
145+
true,
146+
) {
147+
override fun run(indicator: ProgressIndicator) {
148+
indicator.text = I18n.t("kilocode:commitMessage.progress.analyzing")
149+
indicator.isIndeterminate = true
150+
151+
try {
152+
val discoveryResult = fileDiscoveryService.discoverFilesWithResult(project, dataContext)
153+
val files = when (discoveryResult) {
154+
is FileDiscoveryService.FileDiscoveryResult.Success -> discoveryResult.files
155+
is FileDiscoveryService.FileDiscoveryResult.Error -> emptyList()
156+
FileDiscoveryService.FileDiscoveryResult.NoFiles -> emptyList()
157+
}
158+
159+
indicator.text = I18n.t("kilocode:commitMessage.progress.generating")
160+
val result = runBlocking {
161+
commitMessageService.generateCommitMessage(project, workspacePath, files.ifEmpty { null })
162+
}
163+
164+
ApplicationManager.getApplication().invokeLater {
165+
when (result) {
166+
is CommitMessageService.Result.Success -> {
167+
openCommitDialogWithMessage(project, result.message)
168+
}
169+
is CommitMessageService.Result.Error -> {
170+
Messages.showErrorDialog(
171+
project,
172+
result.errorMessage,
173+
I18n.t("kilocode:commitMessage.dialogs.error"),
174+
)
175+
}
176+
}
177+
}
178+
} catch (e: Exception) {
179+
logger.error("Error generating commit message", e)
180+
ApplicationManager.getApplication().invokeLater {
181+
Messages.showErrorDialog(
182+
project,
183+
I18n.t("kilocode:commitMessage.errors.processingError",
184+
mapOf("error" to (e.message ?: I18n.t("kilocode:commitMessage.error.unknown")))),
185+
I18n.t("kilocode:commitMessage.dialogs.error"),
186+
)
187+
}
188+
}
189+
}
190+
})
191+
}
192+
193+
private fun openCommitDialogWithMessage(project: Project, message: String) {
194+
try {
195+
val actionManager = com.intellij.openapi.actionSystem.ActionManager.getInstance()
196+
val commitAction = actionManager.getAction("CheckinProject")
197+
198+
if (commitAction != null) {
199+
project.putUserData(PENDING_COMMIT_MESSAGE_KEY, message)
200+
val dataContext = com.intellij.openapi.actionSystem.impl.SimpleDataContext.getProjectContext(project)
201+
val actionEvent = com.intellij.openapi.actionSystem.AnActionEvent.createFromDataContext(
202+
"GitCommitMessageAction",
203+
null,
204+
dataContext,
205+
)
206+
commitAction.actionPerformed(actionEvent)
207+
}
208+
} catch (e: Exception) {
209+
logger.error("Failed to open commit dialog", e)
210+
}
211+
}
212+
213+
214+
companion object {
215+
val PENDING_COMMIT_MESSAGE_KEY = com.intellij.openapi.util.Key.create<String>("KILOCODE_PENDING_COMMIT_MESSAGE")
216+
}
217+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package ai.kilocode.jetbrains.git
6+
7+
/**
8+
* Shared constants for Git commit message generation functionality.
9+
*/
10+
object CommitMessageConstants {
11+
/**
12+
* VSCode extension command ID for external commit message generation.
13+
*/
14+
const val EXTERNAL_COMMAND_ID = "kilo-code.jetbrains.generateCommitMessage"
15+
16+
/**
17+
* Default timeout in milliseconds for commit message generation requests.
18+
*/
19+
const val RPC_TIMEOUT_MS = 30000L
20+
}

0 commit comments

Comments
 (0)