diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index dbea7374493..1b5b656b85c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -42,6 +42,7 @@ import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.SpellCheckingEditorCustomizationProvider import com.intellij.openapi.editor.colors.CodeInsightColors import com.intellij.openapi.editor.colors.TextAttributesKey @@ -256,6 +257,7 @@ class PromptPanel( private var ready = false private var enhancing = false private var request = 0L + private var deferred = false override val isSendEnabled: Boolean get() = ready && !submitting && (text().isNotEmpty() || attachments.isNotEmpty()) @@ -270,12 +272,22 @@ class PromptPanel( editor.addDocumentListener(object : DocumentListener { override fun documentChanged(e: DocumentEvent) { invalidateEnhancement() + if (e.document.isInBulkUpdate) { + deferEditorSync() + syncButton() + onChange() + return + } syncEditorHeight() triggerCompletion(e) syncHighlights() syncButton() onChange() } + + override fun bulkUpdateFinished(document: Document) { + deferEditorSync() + } }) shell.add(strip, BorderLayout.NORTH) shell.add(editor, BorderLayout.CENTER) @@ -937,6 +949,10 @@ class PromptPanel( @RequiresEdt private fun syncEditorHeight() { + if (editor.document.isInBulkUpdate) { + deferEditorSync() + return + } val before = editor.preferredSize.height val lower = editor.minimumSize.height editor.setPreferredSize(null) @@ -968,6 +984,19 @@ class PromptPanel( repaint() } + @RequiresEdt + private fun deferEditorSync() { + if (deferred) return + deferred = true + ApplicationManager.getApplication().invokeLater { + deferred = false + if (project.isDisposed || editor.document.isInBulkUpdate) return@invokeLater + syncEditorHeight() + syncHighlights() + syncButton() + } + } + @RequiresEdt private fun syncEditorScroll(ed: EditorEx?, overflow: Boolean) { // AS_NEEDED keeps the standard auto-hiding editor scrollbar (appears on diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsState.kt index 3a5989a3325..6de6c44133d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsState.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.context +import ai.kilocode.client.plugin.KiloPluginSettings import ai.kilocode.rpc.dto.CompactionPatchDto import ai.kilocode.rpc.dto.ConfigDto import ai.kilocode.rpc.dto.ConfigPatchDto @@ -9,6 +10,7 @@ internal data class ContextDraft( val auto: Boolean = false, val threshold: String = "", val prune: Boolean = false, + val editor: Boolean = KiloPluginSettings.getAutoEditorContext(), val ignore: List = emptyList(), ) @@ -21,6 +23,7 @@ internal fun contextDraft(config: ConfigDto?): ContextDraft = ContextDraft( auto = config?.compaction?.auto ?: false, threshold = config?.compaction?.threshold_percent?.let(::formatThreshold).orEmpty(), prune = config?.compaction?.prune ?: false, + editor = KiloPluginSettings.getAutoEditorContext(), ignore = config?.watcher?.ignore ?: emptyList(), ) @@ -38,8 +41,11 @@ internal fun savedMatches(base: ContextDraft, draft: ContextDraft): Boolean = base.auto == draft.auto && normalizeThreshold(base.threshold) == normalizeThreshold(draft.threshold) && base.prune == draft.prune && + base.editor == draft.editor && base.ignore == draft.ignore +internal fun localChanged(base: ContextDraft, draft: ContextDraft): Boolean = base.editor != draft.editor + internal fun thresholdStatus(value: String): ThresholdStatus { val text = value.trim() if (text.isBlank()) return ThresholdStatus.VALID diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsUi.kt index d6e4a8a4333..8b156065b82 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/ContextSettingsUi.kt @@ -63,10 +63,23 @@ internal class ContextSettingsUi( startSettings(ContextSettingsContent { updateDraft(it) }) } - override fun change(from: ContextDraft, to: ContextDraft): ConfigPatchDto? = patch(from, to)?.takeIf(::changed) + override fun change(from: ContextDraft, to: ContextDraft): ConfigPatchDto? { + val patch = patch(from, to) ?: return null + if (changed(patch)) return patch + return ConfigPatchDto().takeIf { localChanged(from, to) } + } override fun save(change: ConfigPatchDto, done: (KiloAppStateDto?) -> Unit) { - app.updateConfigAsync(change, done) + val value = draft.editor + if (!changed(change)) { + KiloPluginSettings.setAutoEditorContext(value) + done(appState) + return + } + app.updateConfigAsync(change) { result -> + if (result != null) KiloPluginSettings.setAutoEditorContext(value) + done(result) + } } override fun base(result: KiloAppStateDto): ContextDraft = contextDraft(result.config) @@ -136,11 +149,9 @@ internal class ContextSettingsContent( ) : BaseContentPanel() { private val auto = SettingsToggle { value -> update { copy(auto = value) } } // Editor-context auto-include is a local per-IDE preference in PropertiesComponent (like - // autoApprove), applied immediately on toggle rather than through the CLI-backed draft/apply/ - // reset flow used by the other rows. It stays interactive even when the backend isn't READY. - private val editor = SettingsToggle(KiloPluginSettings.getAutoEditorContext()) { value -> - KiloPluginSettings.setAutoEditorContext(value) - } + // autoApprove). It participates in this page's draft/apply/reset state so the Configurable + // Apply button reflects unsaved local changes, but it is never sent as CLI config. + private val editor = SettingsToggle { value -> update { copy(editor = value) } } private val prune = SettingsToggle { value -> update { copy(prune = value) } } private val threshold = ThresholdField( KiloBundle.message("settings.context.compaction.threshold.placeholder"), @@ -186,9 +197,9 @@ internal class ContextSettingsContent( @RequiresEdt fun sync(draft: ContextDraft, enabled: Boolean) { auto.isSelected = draft.auto - // Local preference: reflects PropertiesComponent and stays enabled regardless of the - // CLI-backed [enabled] gating that applies to the draft-driven rows below. - editor.isSelected = KiloPluginSettings.getAutoEditorContext() + // Local preference: draft-driven, but still enabled regardless of the CLI-backed [enabled] + // gating that applies to the remote config rows below. + editor.isSelected = draft.editor editor.isEnabled = true prune.isSelected = draft.prune threshold.sync(draft.threshold) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt index cb787b2b77d..63d41c27f21 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt @@ -70,6 +70,7 @@ import com.intellij.ui.components.JBLabel import com.intellij.util.Producer import com.intellij.util.ui.EmptyIcon import com.intellij.ui.scale.JBUIScale +import com.intellij.util.DocumentUtil import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import kotlinx.coroutines.CompletableDeferred @@ -501,6 +502,22 @@ class PromptPanelTest : BasePlatformTestCase() { assertEquals("hello", editor.document.text) } + fun `test prompt editor height sync skips bulk document updates`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + val editor = field.getEditor(false)!! + WriteCommandAction.runWriteCommandAction(project) { + DocumentUtil.executeInBulk(editor.document, true) { + editor.document.insertString(0, "hello") + } + } + UIUtil.dispatchAllInvocationEvents() + + assertEquals("hello", editor.document.text) + } + fun `test prompt editor highlights missing mention as wrong reference`() { val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) val field = panel.defaultFocusedComponent as EditorTextField diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt index ac29c470bed..44337dd149a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt @@ -274,7 +274,7 @@ class ContextSettingsUiTest : BasePlatformTestCase() { flushUntil { rpc.configPatches.isNotEmpty() } } - fun `test editor context toggle persists immediately without a config patch`() { + fun `test editor context toggle marks modified and applies without a config patch`() { val panel = requireUi() assertTrue(KiloPluginSettings.getAutoEditorContext()) @@ -282,10 +282,14 @@ class ContextSettingsUiTest : BasePlatformTestCase() { val editor = editorToggle(panel) assertTrue(editor.isEnabled) editor.doClick() + assertTrue(panel.modified()) } + assertTrue(KiloPluginSettings.getAutoEditorContext()) + edt { panel.applyDraft() } assertFalse(KiloPluginSettings.getAutoEditorContext()) edt { UIUtil.dispatchAllInvocationEvents() } + assertFalse(edt { panel.modified() }) assertTrue(rpc.configPatches.isEmpty()) }