diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveConfigurable.kt index 6d0a563125..4ccd9ef31d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveConfigurable.kt @@ -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() { +class AutoApproveConfigurable : DraftReadyConfigurable() { 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 { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveContent.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveContent.kt index b1bc60cedd..0a4da5f927 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveContent.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveContent.kt @@ -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, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUi.kt index 182b6165f3..6725018cc6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUi.kt @@ -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)) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/GranularToolSection.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/GranularToolSection.kt index 11c231ebe7..f0057e557d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/GranularToolSection.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/GranularToolSection.kt @@ -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) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineList.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineList.kt index 94ba89b649..4423d534f4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineList.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineList.kt @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt index e3529d7cb9..5f51f48f0c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt @@ -34,7 +34,3 @@ abstract class DraftReadyConfigurableBase : KiloReadyConfigurabl } abstract class DraftReadyConfigurable : DraftReadyConfigurableBase(), Configurable.NoScroll - -abstract class ScrollableDraftReadyConfigurable : DraftReadyConfigurableBase() { - override fun scrollReadyShell(): Boolean = false -} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsInlineListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsInlineListPanel.kt index 10f88c598d..db3c9e26e7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsInlineListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsInlineListPanel.kt @@ -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() diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 7cac757f61..818a4b5a9f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -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 * diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt index ad53a9745b..7ad65e9f7c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt @@ -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) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt index 5bd2d5e370..6432ed16e9 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt @@ -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>().single() - private fun search(list: SettingsInlineList): javax.swing.text.JTextComponent = - components(list).filterIsInstance().first() - private fun layout(root: Container) { root.setSize(400, root.preferredSize.height.coerceAtLeast(50)) root.doLayout()