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, )