diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 5068db342e2..28c4cd1750d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -156,7 +156,7 @@ class SessionUi( condense = Registry.`is`("kilo.session.condense", true), displayMs = displayMs, open = { item -> manager?.openSession(item) }, - beforeUpdate = { if (opening) false else scroll.atBottom() }, + beforeUpdate = { if (opening) false else scroll.following() }, afterUpdate = { if (!opening) scroll.followBottom(it) }, loaded = ::onSessionLoaded, openProfileAction = ::openProfileSettings, @@ -684,7 +684,7 @@ class SessionUi( private fun sendPrompt(text: String, files: List) { if (text.isBlank() && files.isEmpty()) return prompt.clear() - val follow = scroll.atBottom() + val follow = scroll.following() val action = completion.clientAction(text) if (action != null) { action.action() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt index 70d9ffeec8f..f9685fd2c53 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionScrollTest.kt @@ -498,6 +498,61 @@ class SessionScrollTest : SessionUiTestBase() { assertFalse(jumpButton().isVisible) } + fun `test turn close after modified files keeps pending tail follow`() { + showMessages() + fillTranscript(24) + val bar = scrollBar() + setBottom(bar) + + emit(ChatEventDto.TurnOpen("ses_test")) + drainScroll() + assertBottom(bar) + assertTrue(ui.scroll.following()) + val id = "modified_close_tail" + val pid = "modified_close_part" + emit(ChatEventDto.MessageUpdated("ses_test", message(id).copy(summary = MessageSummaryDto(listOf(modifiedFile())))), flush = false) + emit(ChatEventDto.PartUpdated("ses_test", part(pid, id, "text", "tail line\n".repeat(160))), flush = false) + forceFlushWithoutDispatch() + + emit(ChatEventDto.TurnClose("ses_test", "completed")) + drainScroll() + + assertBottom(bar) + assertTrue(ui.scroll.following()) + assertFalse(jumpButton().isVisible) + + findAll(ui).first().text = "next prompt" + find(ui).send() + settleShort(100) + val text = rpc.prompts.last().third.parts.single().text + val next = "modified_close_next" + emit(ChatEventDto.MessageUpdated("ses_test", message(next)), flush = false) + emit(ChatEventDto.PartUpdated("ses_test", part("modified_close_next_part", next, "text", text)), flush = false) + forceFlush() + drainScroll() + + assertBottom(bar) + assertFalse(jumpButton().isVisible) + } + + fun `test turn close after modified files preserves user scroll position`() { + showMessages() + fillTranscript(24) + val bar = scrollBar() + setValue(bar, bottom(bar) / 2) + val value = bar.value + + emit(ChatEventDto.TurnOpen("ses_test"), flush = false) + emit(ChatEventDto.MessageUpdated("ses_test", message("modified_close_middle").copy(summary = MessageSummaryDto(listOf(modifiedFile())))), flush = false) + emit(ChatEventDto.TurnClose("ses_test", "completed"), flush = false) + forceFlush() + drainScroll() + + assertEquals(value, bar.value) + assertFalse(ui.scroll.following()) + assertTrue(jumpButton().isVisible) + } + fun `test prompt editor growth preserves middle scroll position`() { showMessages() fillTranscript(24)