refactor(jetbrains): page-level auto-approve filter and wildcard labels

This commit is contained in:
kirillk
2026-07-20 16:33:52 -04:00
parent ef8354e921
commit 308c86f81d
10 changed files with 73 additions and 36 deletions
@@ -1,15 +1,19 @@
package ai.kilocode.client.settings.autoapprove
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.ScrollableDraftReadyConfigurable
import ai.kilocode.client.settings.base.DraftReadyConfigurable
import kotlinx.coroutines.CoroutineScope
import javax.swing.JComponent
class AutoApproveConfigurable : ScrollableDraftReadyConfigurable<JComponent>() {
class AutoApproveConfigurable : DraftReadyConfigurable<JComponent>() {
override fun getId(): String = ID
override fun getDisplayName(): String = KiloBundle.message("settings.autoApprove.displayName")
// The page renders its own fixed search field plus a scrollable body, so the shell must not
// add another scroll pane around it.
override fun scrollReadyShell(): Boolean = false
override fun create(cs: CoroutineScope): JComponent = AutoApproveSettingsUi(cs)
companion object {
@@ -29,7 +29,6 @@ internal class AutoApproveContent(
private val granular = GRANULAR_TOOLS.map { (id, kind) -> id to granularSection(id, kind) }
private val tools = SettingsInlineList(
empty = KiloBundle.message("settings.autoApprove.tools.empty"),
search = KiloBundle.message("settings.autoApprove.tools.search"),
onSetLevel = { key, level -> update { setListTool(this, key, level) } },
onInherit = { key -> update { inheritListTool(this, key) } },
picker = picker,
@@ -46,12 +45,24 @@ internal class AutoApproveContent(
tools.syncRows(toolRows(draft), enabled)
}
/** Filter every list on the page by [query], driven by the shared search field. */
@RequiresEdt
fun filter(query: String) {
for ((_, section) in granular) section.filter(query)
tools.filter(query)
}
private fun granularSection(tool: String, kind: ExceptionKind): GranularToolSection {
val addKey = if (kind == ExceptionKind.COMMAND) "addCommand" else "addPath"
val placeholderKey = if (kind == ExceptionKind.COMMAND) "placeholder.command" else "placeholder.path"
val commands = kind == ExceptionKind.COMMAND
val wildcardKey = if (commands) "commands" else "paths"
val emptyKey = if (commands) "commands" else "paths"
val addKey = if (commands) "addCommand" else "addPath"
val placeholderKey = if (commands) "placeholder.command" else "placeholder.path"
return GranularToolSection(
tool,
KiloBundle.message("settings.autoApprove.tool.$tool"),
KiloBundle.message("settings.autoApprove.wildcardLabel.$wildcardKey"),
KiloBundle.message("settings.autoApprove.filters.empty.$emptyKey"),
KiloBundle.message("settings.autoApprove.$addKey"),
KiloBundle.message("settings.autoApprove.$placeholderKey"),
picker,
@@ -4,13 +4,19 @@ import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.BaseSettingsUi
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.log.KiloLog
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.KiloAppStateDto
import ai.kilocode.rpc.dto.KiloAppStatusDto
import com.intellij.openapi.components.service
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.SearchTextField
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBUI
import kotlinx.coroutines.CoroutineScope
import java.awt.BorderLayout
import javax.swing.event.DocumentEvent
internal class AutoApproveSettingsUi(
cs: CoroutineScope,
@@ -23,9 +29,16 @@ internal class AutoApproveSettingsUi(
app,
workspaces,
loginBanner = false,
scroll = false,
) {
private val search = SearchTextField(false)
init {
search.textEditor.emptyText.text = KiloBundle.message("settings.autoApprove.filter")
search.border = JBUI.Borders.empty(UiStyle.Gap.md(), UiStyle.Gap.pad())
search.textEditor.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) = form.filter(search.text)
})
content.add(search, BorderLayout.NORTH)
startSettings(AutoApproveContent({ updateDraft(it) }, picker))
}
@@ -11,8 +11,10 @@ import javax.swing.ListSelectionModel
/** One granular permission tool (`external_directory`, `bash`, `read`, `edit`). */
internal class GranularToolSection(
private val tool: String,
private val tool: String,
description: String,
private val wildcardLabel: String,
emptyText: String,
addLabel: String,
placeholder: String,
picker: LevelPicker,
@@ -24,8 +26,7 @@ internal class GranularToolSection(
) : BaseContentPanel() {
private val wildcard = LevelSelect(onWildcardChange) { onWildcardInherit() }
private val list = SettingsInlineList(
empty = KiloBundle.message("settings.autoApprove.filters.empty"),
search = KiloBundle.message("settings.autoApprove.filters.search"),
empty = emptyText,
addLabel = addLabel,
placeholder = placeholder,
right = toolbarRight(),
@@ -47,7 +48,10 @@ internal class GranularToolSection(
list.syncItems(exceptions(rule), enabled)
}
@RequiresEdt
fun filter(query: String) = list.filter(query)
private fun toolbarRight() = Stack.horizontal(UiStyle.Gap.sm())
.next(JBLabel(KiloBundle.message("settings.autoApprove.defaultSetting")))
.next(JBLabel(wildcardLabel))
.next(wildcard)
}
@@ -63,7 +63,6 @@ internal fun levelChoiceLabel(choice: LevelChoice): String = when (choice) {
*/
internal class SettingsInlineList(
private val empty: String,
private val search: String,
private val addLabel: String? = null,
private val placeholder: String = "",
private val right: JComponent? = null,
@@ -77,6 +76,7 @@ internal class SettingsInlineList(
empty,
SettingsListConfig.Equal,
selectionMode,
showSearch = false,
) {
/** Overridable in tests, mirrors `PatternList.input` in ContextSettingsUi.kt. */
@@ -123,8 +123,6 @@ internal class SettingsInlineList(
override fun toolbarRight(): JComponent? = right
override fun searchPlaceholder(): String = search
private fun promptAdd() {
if (!isEnabled) return
val add = onAdd ?: return
@@ -34,7 +34,3 @@ abstract class DraftReadyConfigurableBase<T : JComponent> : KiloReadyConfigurabl
}
abstract class DraftReadyConfigurable<T : JComponent> : DraftReadyConfigurableBase<T>(), Configurable.NoScroll
abstract class ScrollableDraftReadyConfigurable<T : JComponent> : DraftReadyConfigurableBase<T>() {
override fun scrollReadyShell(): Boolean = false
}
@@ -31,6 +31,7 @@ internal abstract class SettingsInlineListPanel(
emptyText: String,
cfg: SettingsListConfig = SettingsListConfig.Equal,
private val selectionMode: Int = ListSelectionModel.SINGLE_SELECTION,
private val showSearch: Boolean = true,
) : BaseContentPanel() {
private val search = SearchTextField(false)
protected val view = SettingsListView(emptyText, cfg) { key, cellId -> onCell(key, cellId) }
@@ -39,17 +40,26 @@ internal abstract class SettingsInlineListPanel(
@RequiresEdt
protected fun start() {
checkEdt()
search.textEditor.emptyText.text = searchPlaceholder()
view.list.selectionMode = selectionMode
view.minimumSize = JBUI.size(0, minListHeight())
view.list.minimumSize = JBUI.size(0, minListHeight())
view.onSelect = { toolbar?.updateActionsImmediately() }
next(toolbarRow())
gap(UiStyle.Gap.sm())
next(search)
gap(UiStyle.Gap.sm())
if (showSearch) {
search.textEditor.emptyText.text = searchPlaceholder()
next(search)
gap(UiStyle.Gap.sm())
wireSearch()
}
next(view)
wireSearch()
}
/** Filter list rows by [query]. Used by an external search field when the list hides its own. */
@RequiresEdt
fun filter(query: String) {
checkEdt()
view.filter(query)
}
@RequiresEdt
@@ -68,8 +78,10 @@ internal abstract class SettingsInlineListPanel(
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
search.isEnabled = enabled
search.textEditor.isEnabled = enabled
if (showSearch) {
search.isEnabled = enabled
search.textEditor.isEnabled = enabled
}
view.isEnabled = enabled
view.setBusy(!enabled)
toolbar?.updateActionsImmediately()
@@ -345,14 +345,15 @@ settings.autoApprove.default=Default ({0})
settings.autoApprove.level.allow=Allow
settings.autoApprove.level.ask=Ask
settings.autoApprove.level.deny=Deny
settings.autoApprove.defaultSetting=Default Setting
settings.autoApprove.filters.empty=No filters
settings.autoApprove.filters.search=Filter filters
settings.autoApprove.filter=Filter auto-approve rules
settings.autoApprove.wildcardLabel.commands=All commands (*)
settings.autoApprove.wildcardLabel.paths=All paths (*)
settings.autoApprove.filters.empty.commands=No custom commands
settings.autoApprove.filters.empty.paths=No custom paths
settings.autoApprove.tools.empty=No tools
settings.autoApprove.tools.search=Filter tools
settings.autoApprove.add=Add
settings.autoApprove.delete=Delete
settings.autoApprove.delete.description=Delete selected filters
settings.autoApprove.delete.description=Delete selected
settings.autoApprove.addCommand=Add command
settings.autoApprove.addPath=Add path
settings.autoApprove.placeholder.command=e.g. git *
@@ -40,10 +40,12 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID)
}
fun `test auto approve uses platform configurable scrollpane`() {
fun `test auto approve opts out of platform scrollpane`() {
// Auto-Approve renders its own fixed search field and scrollable body, so it must not be
// wrapped in the platform configurable scrollpane.
val auto: Configurable = AutoApproveConfigurable()
val context: Configurable = ContextConfigurable()
assertFalse(auto is Configurable.NoScroll)
assertTrue(auto is Configurable.NoScroll)
assertTrue(context is Configurable.NoScroll)
}
@@ -31,7 +31,7 @@ class SettingsInlineListTest : BasePlatformTestCase() {
list.syncItems(listOf("*.env" to "deny", "*.key" to "deny", "*.pem" to "deny"), true)
layout(list)
search(list).text = "nomatch"
list.filter("nomatch")
layout(list)
assertEquals(0, jbList(list).model.size)
@@ -113,7 +113,7 @@ class SettingsInlineListTest : BasePlatformTestCase() {
}
}
fun `test setEnabled disables search add and list`() {
fun `test setEnabled disables add and list`() {
edt {
val list = list()
list.syncItems(listOf("*.env" to "deny"), true)
@@ -134,7 +134,6 @@ class SettingsInlineListTest : BasePlatformTestCase() {
selection: Int = ListSelectionModel.SINGLE_SELECTION,
): SettingsInlineList = SettingsInlineList(
empty = "Empty",
search = "Search",
addLabel = "Add",
placeholder = "e.g. *.env",
onAdd = onAdd,
@@ -162,9 +161,6 @@ class SettingsInlineListTest : BasePlatformTestCase() {
private fun jbList(list: SettingsInlineList): JBList<*> = components(list).filterIsInstance<JBList<*>>().single()
private fun search(list: SettingsInlineList): javax.swing.text.JTextComponent =
components(list).filterIsInstance<javax.swing.text.JTextComponent>().first()
private fun layout(root: Container) {
root.setSize(400, root.preferredSize.height.coerceAtLeast(50))
root.doLayout()