Skip to content

Commit 67e8f1f

Browse files
committed
feat: Table for prompts.
1 parent 456c119 commit 67e8f1f

File tree

8 files changed

+348
-74
lines changed

8 files changed

+348
-74
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44
### Added
5+
- Table for setting prompts.
56
- Different prompts to choose from.
67
- Bug report link to settings.
78
- Add generate commit action progress indicator.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.github.blarc.ai.commits.intellij.plugin
2+
3+
import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message
4+
import com.intellij.openapi.ui.ValidationInfo
5+
import com.intellij.ui.layout.ValidationInfoBuilder
6+
import com.intellij.util.ui.ColumnInfo
7+
8+
fun <T> createColumn(name: String, formatter: (T) -> String) : ColumnInfo<T, String> {
9+
return object : ColumnInfo<T, String>(name) {
10+
override fun valueOf(item: T): String {
11+
return formatter(item)
12+
}
13+
}
14+
}
15+
16+
fun ValidationInfoBuilder.notBlank(value: String): ValidationInfo? =
17+
if (value.isBlank()) error(message("validation.required")) else null
18+
19+
fun ValidationInfoBuilder.unique(value: String, existingValues: Set<String>): ValidationInfo? =
20+
if (existingValues.contains(value)) error(message("validation.unique")) else null
21+
22+
fun ValidationInfoBuilder.isLong(value: String): ValidationInfo? {
23+
if (value.isBlank()){
24+
return null
25+
}
26+
27+
value.toLongOrNull().let {
28+
if (it == null) {
29+
return error(message("validation.number"))
30+
} else {
31+
return null
32+
}
33+
}
34+
}

src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings.kt

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.github.blarc.ai.commits.intellij.plugin.settings
22

33
import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification
44
import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification
5+
import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.Prompt
56
import com.intellij.credentialStore.CredentialAttributes
67
import com.intellij.credentialStore.Credentials
78
import com.intellij.ide.passwordSafe.PasswordSafe
@@ -15,8 +16,8 @@ import com.intellij.util.xmlb.annotations.OptionTag
1516
import java.util.*
1617

1718
@State(
18-
name = AppSettings.SERVICE_NAME,
19-
storages = [Storage("AICommit.xml")]
19+
name = AppSettings.SERVICE_NAME,
20+
storages = [Storage("AICommit.xml")]
2021
)
2122
class AppSettings : PersistentStateComponent<AppSettings> {
2223

@@ -28,45 +29,25 @@ class AppSettings : PersistentStateComponent<AppSettings> {
2829
var requestSupport = true
2930
var lastVersion: String? = null
3031

31-
var currentPrompt: String = "basic"
32-
var prompts: MutableMap<String, String> = mutableMapOf(
33-
// Generate UUIDs for game objects in Mine.py and call the function in start_game().
34-
"basic" to "Write an insightful but concise Git commit message in a complete sentence in present tense for the " +
35-
"following diff without prefacing it with anything, the response must be in the language {locale} and must" +
36-
"not be longer than 74 characters. The sent text will be the differences between files, where deleted lines" +
37-
" are prefixed with a single minus sign and added lines are prefixed with a single plus sign.\n" +
38-
"{diff}",
39-
// feat: generate unique UUIDs for game objects on Mine game start
40-
"conventional" to "Write a clean and comprehensive commit message in the conventional commit convention. " +
41-
"I'll send you an output of 'git diff --staged' command, and you convert " +
42-
"it into a commit message. " +
43-
"Do NOT preface the commit with anything. " +
44-
"Do NOT add any descriptions to the commit, only commit message. " +
45-
"Use the present tense. " +
46-
"Lines must not be longer than 74 characters. " +
47-
"Use {locale} language to answer.\n" +
48-
"{diff}",
49-
// ✨ feat(mine): Generate objects UUIDs and start team timers on game start
50-
"emoji" to "Write a clean and comprehensive commit messages in the conventional commit convention. " +
51-
"I'll send you an output of 'git diff --staged' command, and you convert " +
52-
"it into a commit message. " +
53-
"Use GitMoji convention to preface the commit. " +
54-
"Do NOT add any descriptions to the commit, only commit message. " +
55-
"Use the present tense. " +
56-
"Lines must not be longer than 74 characters. " +
57-
"Use {locale} language to answer.\n" +
58-
"{diff}",
59-
)
32+
var prompts: MutableMap<String, Prompt> = initPrompts()
33+
var currentPrompt: Prompt = prompts["basic"]!!
6034

6135
companion object {
6236
const val SERVICE_NAME = "com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings"
6337
val instance: AppSettings
6438
get() = ApplicationManager.getApplication().getService(AppSettings::class.java)
6539
}
6640

67-
fun getPrompt(diff: String) = prompts.getOrDefault(currentPrompt, prompts["basic"]!!)
68-
.replace("{locale}", locale.displayName)
69-
.replace("{diff}", diff)
41+
fun getPrompt(diff: String): String {
42+
val content = currentPrompt.content
43+
content.replace("{locale}", locale.displayName)
44+
45+
return if (content.contains("{diff}")) {
46+
content.replace("{diff}", diff)
47+
} else {
48+
"$content\n$diff"
49+
}
50+
}
7051

7152
fun saveOpenAIToken(token: String) {
7253
try {
@@ -84,10 +65,10 @@ class AppSettings : PersistentStateComponent<AppSettings> {
8465

8566
private fun getCredentialAttributes(title: String): CredentialAttributes {
8667
return CredentialAttributes(
87-
title,
88-
null,
89-
this.javaClass,
90-
false
68+
title,
69+
null,
70+
this.javaClass,
71+
false
9172
)
9273
}
9374

@@ -104,6 +85,44 @@ class AppSettings : PersistentStateComponent<AppSettings> {
10485
}
10586
}
10687

88+
private fun initPrompts() = mutableMapOf(
89+
// Generate UUIDs for game objects in Mine.py and call the function in start_game().
90+
"basic" to Prompt("Basic",
91+
"Basic prompt that generates a decent commit message.",
92+
"Write an insightful but concise Git commit message in a complete sentence in present tense for the " +
93+
"following diff without prefacing it with anything, the response must be in the language {locale} and must " +
94+
"NOT be longer than 74 characters. The sent text will be the differences between files, where deleted lines" +
95+
" are prefixed with a single minus sign and added lines are prefixed with a single plus sign.\n" +
96+
"{diff}",
97+
false),
98+
// feat: generate unique UUIDs for game objects on Mine game start
99+
"conventional" to Prompt("Conventional",
100+
"Prompt for commit message in the conventional commit convention.",
101+
"Write a clean and comprehensive commit message in the conventional commit convention. " +
102+
"I'll send you an output of 'git diff --staged' command, and you convert " +
103+
"it into a commit message. " +
104+
"Do NOT preface the commit with anything. " +
105+
"Do NOT add any descriptions to the commit, only commit message. " +
106+
"Use the present tense. " +
107+
"Lines must not be longer than 74 characters. " +
108+
"Use {locale} language to answer.\n" +
109+
"{diff}",
110+
false),
111+
// ✨ feat(mine): Generate objects UUIDs and start team timers on game start
112+
"emoji" to Prompt("Emoji",
113+
"Prompt for commit message in the conventional commit convention with GitMoji convention.",
114+
"Write a clean and comprehensive commit message in the conventional commit convention. " +
115+
"I'll send you an output of 'git diff --staged' command, and you convert " +
116+
"it into a commit message. " +
117+
"Use GitMoji convention to preface the commit. " +
118+
"Do NOT add any descriptions to the commit, only commit message. " +
119+
"Use the present tense. " +
120+
"Lines must not be longer than 74 characters. " +
121+
"Use {locale} language to answer.\n" +
122+
"{diff}",
123+
false)
124+
)
125+
107126
class LocaleConverter : Converter<Locale>() {
108127
override fun toString(value: Locale): String? {
109128
return value.toLanguageTag()
@@ -113,5 +132,4 @@ class AppSettings : PersistentStateComponent<AppSettings> {
113132
return Locale.forLanguageTag(value)
114133
}
115134
}
116-
117-
}
135+
}

src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettingsConfigurable.kt

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,76 +4,123 @@ import com.aallam.openai.api.exception.OpenAIAPIException
44
import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle
55
import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message
66
import com.github.blarc.ai.commits.intellij.plugin.OpenAIService
7+
import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.Prompt
8+
import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.PromptTable
79
import com.intellij.icons.AllIcons
10+
import com.intellij.openapi.actionSystem.AnAction
11+
import com.intellij.openapi.actionSystem.AnActionEvent
812
import com.intellij.openapi.options.BoundConfigurable
913
import com.intellij.openapi.progress.runBackgroundableTask
14+
import com.intellij.openapi.ui.ComboBox
15+
import com.intellij.ui.CommonActionsPanel
16+
import com.intellij.ui.ToolbarDecorator
1017
import com.intellij.ui.components.JBLabel
11-
import com.intellij.ui.components.JBTextArea
1218
import com.intellij.ui.dsl.builder.*
1319
import kotlinx.coroutines.DelicateCoroutinesApi
1420
import kotlinx.coroutines.Dispatchers
1521
import kotlinx.coroutines.GlobalScope
1622
import kotlinx.coroutines.launch
1723
import java.util.*
24+
import javax.swing.JComponent
1825
import javax.swing.JPasswordField
26+
import javax.swing.JScrollPane
1927

2028
class AppSettingsConfigurable : BoundConfigurable(message("settings.general.group.title")) {
2129

2230
private val tokenPasswordField = JPasswordField()
2331
private val verifyLabel = JBLabel()
24-
private val promptTextArea = JBTextArea()
25-
init {
26-
promptTextArea.wrapStyleWord = true
27-
promptTextArea.lineWrap = true
28-
promptTextArea.isEditable = false
29-
}
32+
private val promptTable = PromptTable()
33+
private lateinit var toolbarDecorator: ToolbarDecorator
34+
private lateinit var promptComboBox: Cell<ComboBox<Prompt>>
3035
override fun createPanel() = panel {
3136

3237
row {
3338
cell(tokenPasswordField)
34-
.label(message("settings.openAIToken"))
35-
.bindText(
36-
{ AppSettings.instance.getOpenAIToken().orEmpty() },
37-
{ AppSettings.instance.saveOpenAIToken(it)}
38-
)
39-
.align(Align.FILL)
40-
.resizableColumn()
41-
.focused()
39+
.label(message("settings.openAIToken"))
40+
.bindText(
41+
{ AppSettings.instance.getOpenAIToken().orEmpty() },
42+
{ AppSettings.instance.saveOpenAIToken(it) }
43+
)
44+
.align(Align.FILL)
45+
.resizableColumn()
46+
.focused()
4247
button(message("settings.verifyToken")) {
4348
verifyToken()
4449
}.align(AlignX.RIGHT)
4550
}
4651
row {
4752
comment(message("settings.openAITokenComment"))
48-
.align(AlignX.LEFT)
53+
.align(AlignX.LEFT)
4954
cell(verifyLabel)
50-
.align(AlignX.RIGHT)
55+
.align(AlignX.RIGHT)
5156
}
5257
row {
5358
comboBox(Locale.getAvailableLocales().toList().sortedBy { it.displayName }, AppSettingsListCellRenderer())
54-
.label(message("settings.locale"))
55-
.bindItem(AppSettings.instance::locale.toNullableProperty())
59+
.label(message("settings.locale"))
60+
.bindItem(AppSettings.instance::locale.toNullableProperty())
5661
}
5762
row {
58-
comboBox(AppSettings.instance.prompts.keys.toList(), AppSettingsListCellRenderer())
59-
.label(message("settings.prompt"))
60-
.bindItem(AppSettings.instance::currentPrompt.toNullableProperty())
61-
.onChanged { promptTextArea.text = AppSettings.instance.prompts[it.item] }
63+
promptComboBox = comboBox(AppSettings.instance.prompts.values, AppSettingsListCellRenderer())
64+
.label(message("settings.prompt"))
65+
.bindItem(AppSettings.instance::currentPrompt.toNullableProperty())
6266
}
6367
row {
64-
cell(promptTextArea)
65-
.bindText(
66-
{ AppSettings.instance.getPrompt("") },
67-
{ }
68-
)
69-
.align(Align.FILL)
70-
.resizableColumn()
68+
toolbarDecorator = ToolbarDecorator.createDecorator(promptTable.table)
69+
.setAddAction {
70+
promptTable.addPrompt().let {
71+
promptComboBox.component.addItem(it)
72+
}
73+
}
74+
.setEditAction {
75+
promptTable.editPrompt()?.let {
76+
promptComboBox.component.removeItem(it.first)
77+
promptComboBox.component.addItem(it.second)
78+
}
79+
}
80+
.setEditActionUpdater {
81+
updateActionAvailability(CommonActionsPanel.Buttons.EDIT)
82+
true
83+
}
84+
.setRemoveAction {
85+
promptTable.removePrompt()?.let {
86+
promptComboBox.component.removeItem(it)
87+
}
88+
}
89+
.setRemoveActionUpdater {
90+
updateActionAvailability(CommonActionsPanel.Buttons.REMOVE)
91+
true
92+
}
93+
.disableUpDownActions()
94+
95+
cell(toolbarDecorator.createPanel())
96+
.align(Align.FILL)
7197
}.resizableRow()
98+
7299
row {
73100
browserLink(message("settings.report-bug"), AICommitsBundle.URL_BUG_REPORT.toString())
74101
}
75102
}
76103

104+
private fun updateActionAvailability(action: CommonActionsPanel.Buttons) {
105+
val selectedRow = promptTable.table.selectedRow
106+
val selectedPrompt = promptTable.table.items[selectedRow]
107+
toolbarDecorator.actionsPanel.setEnabled(action, selectedPrompt.canBeChanged)
108+
}
109+
110+
override fun isModified(): Boolean {
111+
return super.isModified() || promptTable.isModified()
112+
}
113+
114+
override fun apply() {
115+
promptTable.apply()
116+
super.apply()
117+
}
118+
119+
override fun reset() {
120+
promptTable.reset()
121+
super.reset()
122+
}
123+
77124
@OptIn(DelicateCoroutinesApi::class)
78125
private fun verifyToken() {
79126
runBackgroundableTask(message("settings.verify.running")) {
@@ -89,12 +136,10 @@ class AppSettingsConfigurable : BoundConfigurable(message("settings.general.grou
89136
OpenAIService.instance.verifyToken(String(tokenPasswordField.password))
90137
verifyLabel.text = message("settings.verify.valid")
91138
verifyLabel.icon = AllIcons.General.InspectionsOK
92-
}
93-
catch (e: OpenAIAPIException) {
139+
} catch (e: OpenAIAPIException) {
94140
verifyLabel.text = message("settings.verify.invalid", e.statusCode)
95141
verifyLabel.icon = AllIcons.General.InspectionsError
96-
}
97-
catch (e: Exception) {
142+
} catch (e: Exception) {
98143
verifyLabel.text = message("settings.verify.invalid", "Unknown")
99144
verifyLabel.icon = AllIcons.General.InspectionsError
100145
}

src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettingsListCellRenderer.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.github.blarc.ai.commits.intellij.plugin.settings
22

3+
import ai.grazie.utils.capitalize
4+
import com.github.blarc.ai.commits.intellij.plugin.settings.prompt.Prompt
35
import java.awt.Component
46
import java.util.*
57
import javax.swing.DefaultListCellRenderer
@@ -17,6 +19,9 @@ class AppSettingsListCellRenderer : DefaultListCellRenderer() {
1719
if (value is Locale) {
1820
text = value.displayName
1921
}
22+
if (value is Prompt) {
23+
text = value.name
24+
}
2025
return component
2126
}
2227
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.github.blarc.ai.commits.intellij.plugin.settings.prompt
2+
3+
data class Prompt(
4+
var name: String = "",
5+
var description: String = "",
6+
var content: String = "",
7+
var canBeChanged: Boolean = true
8+
)

0 commit comments

Comments
 (0)