diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt index d0b1150101..199039b557 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt @@ -42,7 +42,9 @@ internal class PermissionDiffView private constructor( @RequiresEdt fun setDiffs(value: List) { - render(value.map(::dto)) + val dtos = value.map(::dto) + if (items == dtos) return + render(dtos) } @RequiresEdt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt index 59e624f4a5..2eaf44cf31 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt @@ -99,7 +99,7 @@ class PermissionView( } private var md: MdView? = null - private val diffViews = mutableListOf() + private var diffView: PermissionDiffView? = null private val ID_DENY = "deny" private val ID_RUN = "run" @@ -165,13 +165,13 @@ class PermissionView( fun setDiffOpener(openDiff: SessionDiffOpener, sessionId: String?) { this.openDiff = openDiff this.sessionId = sessionId - for (view in diffViews) view.setDiffOpener(openDiff, sessionId, requestId) + diffView?.setDiffOpener(openDiff, sessionId, requestId) } @RequiresEdt fun setHoverSink(sink: (PartView, Boolean) -> Unit) { hover = sink - for (view in diffViews) view.hover = sink + diffView?.hover = sink } /** Hide this view and clear the active request id. */ @@ -195,9 +195,7 @@ class PermissionView( desc.foreground = SessionUiStyle.Text.Secondary.foreground() rules.applyStyle(style) md?.let { applyCodeStyle(it) } - for (dv in diffViews) { - dv.applyStyle(style) - } + diffView?.applyStyle(style) } @RequiresEdt @@ -208,16 +206,26 @@ class PermissionView( @RequiresEdt private fun syncDiffs(diffs: List) { - disposeDiffs() - diffRow.isVisible = diffs.isNotEmpty() - if (diffs.isNotEmpty()) { + if (diffs.isEmpty()) { + disposeDiffs() + return + } + // Retain the card across the RESPONDING/ERROR re-renders of the same request so an + // expanded inline preview is not torn down; setDiffs updates it in place. The card is + // disposed in hideView when the request resolves, so a new request always starts fresh. + val existing = diffView + if (existing != null) { + existing.setDiffOpener(openDiff, sessionId, requestId) + existing.setDiffs(diffs) + } else { val dv = PermissionDiffView(diffs, openFile, selection) dv.setDiffOpener(openDiff, sessionId, requestId) dv.hover = hover dv.applyStyle(style) - diffViews.add(dv) + diffView = dv diffRow.add(dv) } + diffRow.isVisible = true diffRow.revalidate() diffRow.repaint() } @@ -418,8 +426,8 @@ class PermissionView( @RequiresEdt private fun disposeDiffs() { - for (view in diffViews) Disposer.dispose(view) - diffViews.clear() + diffView?.let(Disposer::dispose) + diffView = null diffRow.removeAll() diffRow.isVisible = false } @@ -443,7 +451,7 @@ class PermissionView( internal fun runButtonForTest() = buttons(this).first { it.text == KiloBundle.message("session.permission.allow") || it.text == KiloBundle.message("session.permission.allow.once") } internal fun denyButtonForTest() = buttons(this).first { it.text == KiloBundle.message("session.permission.reject") } internal fun codeLabelsForTest() = codeEditors() - internal fun diffViewsForTest() = diffViews.toList() + internal fun diffViewsForTest() = listOfNotNull(diffView) internal fun headerFontForTest() = textAreas(this).first { it.font.isBold }.font internal fun rulesForTest() = rules diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt index a0b936ee33..5d56c3f788 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt @@ -1147,6 +1147,39 @@ class PermissionViewTest : BasePlatformTestCase() { } } + fun `test diff view is retained and keeps expansion across same-request re-render`() { + fun request(state: PermissionRequestState) = Permission( + id = "perm_retain_diff", + sessionId = "ses", + name = "edit", + patterns = listOf("src/A.kt"), + always = emptyList(), + meta = PermissionMeta( + fileDiffs = listOf( + PermissionFileDiff( + file = "src/A.kt", + patch = "@@ -1 +1 @@\n-old\n+new", + additions = 1, + deletions = 1, + ), + ), + ), + state = state, + ) + + view.show(request(PermissionRequestState.PENDING)) + val diff = view.diffViewsForTest().single() + diff.expand() + assertTrue(diff.isExpanded()) + + // The RESPONDING tick re-renders the same request; the card must survive so the + // expanded inline preview is not torn down. + view.show(request(PermissionRequestState.RESPONDING)) + + assertSame(diff, view.diffViewsForTest().single()) + assertTrue("Expanded preview should persist across re-render", diff.isExpanded()) + } + fun `test stale rule command fields are released on rebuild`() { val base = EditorFactory.getInstance().allEditors.size view.show(permissionWithRules("perm_rules_rebuild", listOf("git status *")))