From b3f68e0a2ac9ecf44e3dadfec54e41f6dd1f8273 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 27 Aug 2026 18:10:43 -0400 Subject: [PATCH] fix(jetbrains): warn when a provider ends a response unfinished A turn whose provider ended the stream without a terminal stop reason closed as "completed" with no message error and no session error, so the session simply went idle and the truncated answer had no explanation. Plumb the assistant finish reason through to the frontend and render a warning outcome card for "unknown" and "other", with the raw reason on the icon tooltip. The notice is seeded from history so it survives reopening the session. "length" is excluded because the CLI already writes a visible warning text part for it. Also broaden the branch/changes refresh from Idle to any non-busy state, so a turn that edited files still refreshes the git indicators when it ends incomplete, failed, interrupted, or in error. --- .changeset/jetbrains-incomplete-response.md | 5 ++ .../kilocode/backend/cli/KiloCliDataParser.kt | 1 + .../backend/cli/KiloCliDataParserTest.kt | 2 + .../ai/kilocode/client/session/SessionUi.kt | 2 +- .../session/controller/SessionController.kt | 17 ++++-- .../client/session/model/SessionState.kt | 2 +- .../client/session/model/TurnOutcome.kt | 19 +++++-- .../session/ui/SessionMessageListPanel.kt | 2 +- .../session/views/SessionOutcomeView.kt | 11 +++- .../resources/messages/KiloBundle.properties | 3 + .../messages/KiloBundle_ar.properties | 3 + .../messages/KiloBundle_bs.properties | 3 + .../messages/KiloBundle_da.properties | 3 + .../messages/KiloBundle_de.properties | 3 + .../messages/KiloBundle_es.properties | 3 + .../messages/KiloBundle_fr.properties | 3 + .../messages/KiloBundle_ja.properties | 3 + .../messages/KiloBundle_ko.properties | 3 + .../messages/KiloBundle_nl.properties | 3 + .../messages/KiloBundle_no.properties | 3 + .../messages/KiloBundle_pl.properties | 3 + .../messages/KiloBundle_pt_BR.properties | 3 + .../messages/KiloBundle_ru.properties | 3 + .../messages/KiloBundle_th.properties | 3 + .../messages/KiloBundle_tr.properties | 3 + .../messages/KiloBundle_uk.properties | 3 + .../messages/KiloBundle_zh_CN.properties | 3 + .../messages/KiloBundle_zh_TW.properties | 3 + .../client/session/SessionUiLayoutTest.kt | 17 ++++++ .../session/controller/SessionRecoveryTest.kt | 56 +++++++++++++++++++ .../session/controller/TurnLifecycleTest.kt | 49 +++++++++++++++- .../session/ui/SessionMessageListPanelTest.kt | 12 ++++ .../session/views/SessionOutcomeViewTest.kt | 31 ++++++++++ .../kotlin/ai/kilocode/rpc/dto/ChatDto.kt | 1 + 34 files changed, 271 insertions(+), 13 deletions(-) create mode 100644 .changeset/jetbrains-incomplete-response.md diff --git a/.changeset/jetbrains-incomplete-response.md b/.changeset/jetbrains-incomplete-response.md new file mode 100644 index 0000000000..706000bf2e --- /dev/null +++ b/.changeset/jetbrains-incomplete-response.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show a warning when a provider ends a response without signalling that it finished, instead of quietly returning the session to idle. The warning also appears when reopening the session. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index a7f1afaefc..1c74c4dfc7 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -1141,6 +1141,7 @@ object KiloCliDataParser { parentID = obj.str("parentID"), cost = obj.num("cost"), tokens = tokens?.let(::parseTokens), + finish = obj.str("finish"), error = error?.let { parseError(it) }, summary = summary, ) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index e42e03c81a..eada611f78 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -1610,6 +1610,7 @@ class KiloCliDataParserTest { "info": { "id": "m1", "sessionID": "s1", "role": "assistant", "time": { "created": 1.0, "completed": 2.0 }, + "finish": "unknown", "tokens": { "input": 100, "output": 50, "reasoning": 10, "cache": { "read": 20, "write": 5 } }, "cost": 0.005 }, @@ -1624,6 +1625,7 @@ class KiloCliDataParserTest { assertEquals(10L, info.tokens?.reasoning) assertEquals(20L, info.tokens?.cacheRead) assertEquals(5L, info.tokens?.cacheWrite) + assertEquals("unknown", info.finish) assertEquals(0.005, info.cost) assertEquals(2.0, info.time.completed) } 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 b340f6c8be..3d1d4f28c2 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 @@ -1099,7 +1099,7 @@ class SessionUi( private fun onStateChanged(state: SessionState) { if (disposed) return val busy = state.isBusy() - if (wasBusy && state is SessionState.Idle) { + if (wasBusy && !busy) { refreshBranchChanges() refreshBranch() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 07a16df0a9..bbac40eb24 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -1513,7 +1513,13 @@ class SessionController( } private fun seedOutcome() { - val err = model.messages().lastOrNull { it.info.role == "assistant" }?.info?.error ?: return + val tail = model.messages().lastOrNull { it.info.role == "assistant" } ?: return + val err = tail.info.error + if (err == null) { + val ended = TurnOutcome.incomplete(tail.info.finish) ?: return + model.setState(SessionState.TurnEnded(ended, tail.info.finish)) + return + } if (err.aborted) { model.setState(SessionState.TurnEnded(Outcome.INTERRUPTED)) return @@ -1600,11 +1606,14 @@ class SessionController( if (current is SessionState.AwaitingPermission) return if (current is SessionState.LoginRequired) return if (current is SessionState.Error && event.reason != "completed") return - val ended = TurnOutcome.classify(event.reason) + val finish = model.messages().lastOrNull { it.info.role == "assistant" }?.info?.finish + val ended = if (current is SessionState.Error) null else TurnOutcome.classify(event.reason, finish) + if (event.reason == "completed") { + capture("Task Completed", sessionProps(event.sessionID) + mapOf("finish" to (finish ?: "none"))) + } when { - ended != null -> model.setState(SessionState.TurnEnded(ended)) + ended != null -> model.setState(SessionState.TurnEnded(ended, finish)) event.reason == "completed" -> { - capture("Task Completed", sessionProps(event.sessionID)) model.setState(SessionState.Idle) } current is SessionState.Busy || current is SessionState.Retry || current is SessionState.Offline -> model.setState(SessionState.Idle) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt index 5223710761..addd953159 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt @@ -22,7 +22,7 @@ sealed class SessionState { data class Error(val message: String, val kind: String? = null) : SessionState() - data class TurnEnded(val outcome: Outcome) : SessionState() + data class TurnEnded(val outcome: Outcome, val finish: String? = null) : SessionState() data class LoginRequired(val message: String) : SessionState() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/TurnOutcome.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/TurnOutcome.kt index 009cd27834..c63a5d9167 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/TurnOutcome.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/TurnOutcome.kt @@ -1,15 +1,26 @@ package ai.kilocode.client.session.model -enum class Outcome { INTERRUPTED, FAILED } +enum class Outcome { INTERRUPTED, FAILED, INCOMPLETE } object TurnOutcome { /** - * Maps a `session.turn.close` reason to the outcome the transcript should show. `completed` and - * `superseded` are normal endings and return null so the session simply falls back to idle. + * Finish reasons that mean the provider ended the response without signalling completion. + * + * "length" is excluded: the CLI already writes a visible warning text part for it, so an outcome + * card would repeat the same message. */ - fun classify(reason: String): Outcome? = when (reason) { + private val bad = setOf("unknown", "other") + + fun incomplete(finish: String?): Outcome? = if (finish in bad) Outcome.INCOMPLETE else null + + /** + * Maps a `session.turn.close` reason plus assistant finish reason to the outcome the transcript + * should show. `superseded` is a normal handoff and returns null so the follow-up turn decides. + */ + fun classify(reason: String, finish: String? = null): Outcome? = when (reason) { "interrupted" -> Outcome.INTERRUPTED "error" -> Outcome.FAILED + "completed" -> incomplete(finish) else -> null } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index 25ed209816..7d890d4945 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -537,7 +537,7 @@ class SessionMessageListPanel( // A failed turn close carries no message of its own; when the tail message explains // itself the generic "stopped with an error" line is noise next to that card. if (state.outcome == Outcome.FAILED && explained() != null) outcome?.showRetry() - else outcome?.showOutcome(state.outcome) + else outcome?.showOutcome(state.outcome, state.finish) } else -> { setHiddenQuestionTool(null) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionOutcomeView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionOutcomeView.kt index b4e37614a3..5e816a6c83 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionOutcomeView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionOutcomeView.kt @@ -79,7 +79,7 @@ class SessionOutcomeView( * outline. Only a model/provider failure gets the error card treatment. */ @RequiresEdt - fun showOutcome(outcome: Outcome) { + fun showOutcome(outcome: Outcome, finish: String? = null) { when (outcome) { Outcome.INTERRUPTED -> { setOutlined(false) @@ -95,6 +95,15 @@ class SessionOutcomeView( setHeader(title, KiloBundle.message("session.outcome.failed.description")) syncRetry(true) } + + Outcome.INCOMPLETE -> { + val title = KiloBundle.message("session.outcome.incomplete.title") + val tip = finish?.let { KiloBundle.message("session.outcome.incomplete.reason", it) } ?: title + setOutlined(true) + setHeaderIcon(AllIcons.General.Warning, tip) + setHeader(title, KiloBundle.message("session.outcome.incomplete.description")) + syncRetry(false) + } } setContentPadding() setContent(null) 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 08025006fa..a191ae7fa8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -214,6 +214,9 @@ session.error.title=Request failed session.error.unknown=Unknown error session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index eadd01397d..22ac319728 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=خطأ غير معروف session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index 17aac1c4d1..554e887d52 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Nepoznata greška session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index 1c9219c5a1..aa92b44bfb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Ukendt fejl session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index bc3fa9c07e..412468c861 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Unbekannter Fehler session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index 1486949c5a..940fcc2ccf 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Error desconocido session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index d8c8b1af60..d4e36b8b42 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Erreur inconnue session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index 7fddcbca62..df37ed821a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=不明なエラー session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index da14ee1a9f..b423b42838 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=알 수 없는 오류 session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index e22bec5c18..c89f2f6132 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Onbekende fout session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index fd5764f09f..79955b90e4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Ukjent feil session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index e5fea16eac..262d930468 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Nieznany błąd session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 2773254f36..cde7c79756 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Erro desconhecido session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index 4feff791d3..be2c1d81db 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Неизвестная ошибка session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index 255f673b95..dfe1696afb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=ข้อผิดพลาดที่ไม่ทราบ session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index a11dd56ef8..79533a219a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Bilinmeyen hata session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index 1a5ff965ff..52d5ecd0ac 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=Невідома помилка session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index 7f557f6c35..f5c1e7ef31 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=未知错误 session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index e812e31894..758fd9e5a4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -77,6 +77,9 @@ session.error.title=Request failed session.error.unknown=未知錯誤 session.outcome.failed.description=The model stopped this turn with an error. session.outcome.failed.title=Response failed +session.outcome.incomplete.title=Response may be incomplete +session.outcome.incomplete.description=The provider ended this response without signalling that it finished. +session.outcome.incomplete.reason=Technical finish reason: {0} session.outcome.retry=Retry session.outcome.interrupted.note=Stopped diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index 9b2afc4400..f358966768 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -7,6 +7,7 @@ import ai.kilocode.client.session.model.PermissionMeta import ai.kilocode.client.session.model.Question import ai.kilocode.client.session.model.QuestionItem import ai.kilocode.client.session.model.QuestionOption +import ai.kilocode.client.session.model.Outcome import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.ui.ConnectionPanel import ai.kilocode.client.session.ui.empty.EmptySessionPanel @@ -284,6 +285,22 @@ class SessionUiLayoutTest : SessionUiTestBase() { workspaceRpc.branchDiffs.clear() workspaceRpc.branchDiffs.add(DiffFileDto("src/C.kt", 1, 0)) + controller().model.setState(SessionState.Busy("running")) + controller().model.setState(SessionState.TurnEnded(Outcome.INCOMPLETE, "unknown")) + settle() + + assertEquals(1 to 0, badge.stats()) + + workspaceRpc.branchDiffs.clear() + workspaceRpc.branchDiffs.add(DiffFileDto("src/D.kt", 5, 2)) + controller().model.setState(SessionState.Busy("running")) + controller().model.setState(SessionState.Error("failed")) + settle() + + assertEquals(5 to 2, badge.stats()) + + workspaceRpc.branchDiffs.clear() + workspaceRpc.branchDiffs.add(DiffFileDto("src/E.kt", 1, 0)) controller().model.setRevert(SessionRevertDto("msg1", "part1", diff = "patch")) settle() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt index 153016aa3e..08b454e674 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt @@ -265,6 +265,62 @@ class SessionRecoveryTest : SessionControllerTestBase() { ) } + fun `test incomplete tail recovers even when the server reports idle`() { + rpc.statuses.value = mapOf("ses_test" to SessionStatusDto("idle")) + rpc.history.add(MessageWithPartsDto( + msg("msg1", "ses_test", "assistant").copy(finish = "unknown"), + emptyList(), + )) + + appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5")) + projectRpc.state.value = workspaceReady() + val m = controller("ses_test") + flush() + + val state = m.model.state + assertTrue(state is SessionState.TurnEnded) + assertEquals( + ai.kilocode.client.session.model.Outcome.INCOMPLETE, + (state as SessionState.TurnEnded).outcome, + ) + assertEquals("unknown", state.finish) + } + + fun `test normal finish does not recover an outcome`() { + rpc.statuses.value = mapOf("ses_test" to SessionStatusDto("idle")) + rpc.history.add(MessageWithPartsDto( + msg("msg1", "ses_test", "assistant").copy(finish = "stop"), + emptyList(), + )) + + appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5")) + projectRpc.state.value = workspaceReady() + val m = controller("ses_test") + flush() + + assertEquals(SessionState.Idle, m.model.state) + } + + fun `test tail error wins over incomplete finish during recovery`() { + rpc.statuses.value = mapOf("ses_test" to SessionStatusDto("idle")) + rpc.history.add(MessageWithPartsDto( + msg("msg1", "ses_test", "assistant").copy( + finish = "unknown", + error = MessageErrorDto(type = "APIError", message = "missing credentials"), + ), + emptyList(), + )) + + appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5")) + projectRpc.state.value = workspaceReady() + val m = controller("ses_test") + flush() + + val state = m.model.state + assertTrue(state is SessionState.Error) + assertEquals("missing credentials", (state as SessionState.Error).message) + } + /** An unrecognised status carries no live work either, so the transcript still decides. */ fun `test unknown status falls through to the failed tail`() { rpc.statuses.value = mapOf("ses_test" to SessionStatusDto("something-new")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt index a2fc73d2b6..fdd0b60626 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt @@ -210,6 +210,43 @@ class TurnLifecycleTest : SessionControllerTestBase() { ) } + fun `test TurnClose completed with unknown finish shows incomplete outcome`() { + val (m, _, _) = prompted() + + emit(ChatEventDto.TurnOpen("ses_test")) + emit(ChatEventDto.MessageUpdated("ses_test", msg("msg_assistant", "ses_test", "assistant").copy(finish = "unknown"))) + emit(ChatEventDto.TurnClose("ses_test", "completed")) + + assertSession( + """ + assistant#msg_assistant + + [code] [kilo/gpt-5] [incomplete] + """, + m, + ) + assertTrue(appRpc.telemetry.any { + it.event == "Task Completed" && it.properties["finish"] == "unknown" + }) + } + + fun `test TurnClose completed with length finish stays idle`() { + val (m, _, _) = prompted() + + emit(ChatEventDto.TurnOpen("ses_test")) + emit(ChatEventDto.MessageUpdated("ses_test", msg("msg_assistant", "ses_test", "assistant").copy(finish = "length"))) + emit(ChatEventDto.TurnClose("ses_test", "completed")) + + assertSession( + """ + assistant#msg_assistant + + [code] [kilo/gpt-5] [idle] + """, + m, + ) + } + fun `test abort error waits for interrupted outcome`() { val (m, _, _) = prompted() @@ -244,11 +281,14 @@ class TurnLifecycleTest : SessionControllerTestBase() { val (m, _, _) = prompted() emit(ChatEventDto.Error("ses_test", MessageErrorDto(type = "timeout", message = "Timed out"))) + emit(ChatEventDto.MessageUpdated("ses_test", msg("msg_assistant", "ses_test", "assistant").copy(finish = "unknown"))) emit(ChatEventDto.TurnClose("ses_test", "completed")) // "completed" always wins over error assertSession( """ + assistant#msg_assistant + [code] [kilo/gpt-5] [idle] """, m, @@ -271,10 +311,17 @@ class TurnLifecycleTest : SessionControllerTestBase() { } fun `test turn outcome classifier`() { + assertEquals(Outcome.INCOMPLETE, TurnOutcome.classify("completed", "unknown")) + assertEquals(Outcome.INCOMPLETE, TurnOutcome.classify("completed", "other")) + assertNull(TurnOutcome.classify("completed", "stop")) + assertNull(TurnOutcome.classify("completed", "length")) + assertNull(TurnOutcome.classify("completed", "tool-calls")) assertNull(TurnOutcome.classify("completed")) - assertNull(TurnOutcome.classify("superseded")) + assertNull(TurnOutcome.classify("superseded", "unknown")) assertEquals(Outcome.INTERRUPTED, TurnOutcome.classify("interrupted")) + assertEquals(Outcome.INTERRUPTED, TurnOutcome.classify("interrupted", "unknown")) assertEquals(Outcome.FAILED, TurnOutcome.classify("error")) + assertEquals(Outcome.FAILED, TurnOutcome.classify("error", "unknown")) } fun `test TurnClose completed preserves AwaitingQuestion state`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt index 0b3a1dd758..98d8ccba83 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt @@ -235,6 +235,18 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { assertNotNull(button(ov, KiloBundle.message("session.outcome.retry"))) } + fun `test incomplete outcome shows footer without message failure card`() { + val item = panelWithRetry { true } + model.upsertMessage(msg("a1", "assistant").copy(finish = "unknown")) + model.setState(SessionState.TurnEnded(Outcome.INCOMPLETE, "unknown")) + + val ov = find(item)!! + assertTrue(cards(item, "a1").isEmpty()) + assertNotNull(text(ov, KiloBundle.message("session.outcome.incomplete.title"))) + assertNotNull(text(ov, KiloBundle.message("session.outcome.incomplete.description"))) + assertNull(button(ov, KiloBundle.message("session.outcome.retry"))) + } + fun `test failure landing after the error state still collapses the footer`() { val item = panelWithRetry { true } model.upsertMessage(msg("a1", "assistant")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/SessionOutcomeViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/SessionOutcomeViewTest.kt index b6c9b0f5a2..6b30181f4c 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/SessionOutcomeViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/SessionOutcomeViewTest.kt @@ -101,11 +101,13 @@ class SessionOutcomeViewTest : BasePlatformTestCase() { edt { val view = SessionOutcomeView() view.showOutcome(Outcome.INTERRUPTED) + view.showOutcome(Outcome.INCOMPLETE, "unknown") view.showOutcome(Outcome.FAILED) assertNotNull(findText(view, KiloBundle.message("session.outcome.failed.title"))) assertNotNull(findText(view, KiloBundle.message("session.outcome.failed.description"))) assertNull(findText(view, KiloBundle.message("session.outcome.interrupted.note"))) + assertNull(findText(view, KiloBundle.message("session.outcome.incomplete.title"))) assertIcons(view, AllIcons.General.Error) } } @@ -164,6 +166,35 @@ class SessionOutcomeViewTest : BasePlatformTestCase() { } } + fun `test incomplete outcome shows warning without retry`() { + edt { + val view = SessionOutcomeView(retry = {}) + view.showOutcome(Outcome.INCOMPLETE, "unknown") + + assertTrue(view.isVisible) + assertNotNull(findText(view, KiloBundle.message("session.outcome.incomplete.title"))) + assertNotNull(findText(view, KiloBundle.message("session.outcome.incomplete.description"))) + assertIcons(view, AllIcons.General.Warning) + assertTrue(findAll(view).any { + it.icon == AllIcons.General.Warning && + it.toolTipText == KiloBundle.message("session.outcome.incomplete.reason", "unknown") + }) + assertNull("An incomplete completed message has no Retry action", retryButton(view)) + } + } + + fun `test incomplete outcome falls back to title tooltip`() { + edt { + val view = SessionOutcomeView(retry = {}) + view.showOutcome(Outcome.INCOMPLETE) + + assertTrue(findAll(view).any { + it.icon == AllIcons.General.Warning && + it.toolTipText == KiloBundle.message("session.outcome.incomplete.title") + }) + } + } + fun `test readonly outcome view offers no retry`() { edt { val view = SessionOutcomeView(retry = null) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index 9a0318477f..a48f05bc5a 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -17,6 +17,7 @@ data class MessageDto( val parentID: String? = null, val cost: Double? = null, val tokens: TokensDto? = null, + val finish: String? = null, val error: MessageErrorDto? = null, val summary: MessageSummaryDto? = null, )