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.
This commit is contained in:
kirillk
2026-08-27 18:10:43 -04:00
parent 56dc51e8a6
commit b3f68e0a2a
34 changed files with 271 additions and 13 deletions
@@ -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.
@@ -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,
)
@@ -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)
}
@@ -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()
}
@@ -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)
@@ -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()
@@ -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
}
}
@@ -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)
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
@@ -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"))
@@ -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`() {
@@ -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<SessionOutcomeView>(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"))
@@ -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<JBLabel>(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<JBLabel>(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)
@@ -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,
)