From aaf2820162363a14f19bd43c8e8b1606f6cafe83 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 10 Aug 2026 09:48:35 -0400 Subject: [PATCH] fix(jetbrains): handle prompt bulk updates and context apply Skip prompt editor height and highlight recalculation while the editor document is in bulk update, then schedule one refresh after bulk mode exits so undo/redo cannot trigger UnexpectedBulkUpdateStateException. Also include the local Auto-Include Editor Context setting in the Context page draft state so toggling it marks the configurable modified and Apply persists it without sending a CLI config patch. --- .../client/session/ui/prompt/PromptPanel.kt | 29 +++++++++++++++++ .../settings/context/ContextSettingsState.kt | 6 ++++ .../settings/context/ContextSettingsUi.kt | 31 +++++++++++++------ .../client/session/ui/PromptPanelTest.kt | 17 ++++++++++ .../settings/context/ContextSettingsUiTest.kt | 6 +++- 5 files changed, 78 insertions(+), 11 deletions(-) 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()) }