fix(chat): keep interrupted replies and notices in place (#1413)
This commit is contained in:
6 files changed
+188
-3
No files matched your search
@@ -1117,6 +1117,9 @@ class ChatViewModel(
|
||||
}
|
||||
|
||||
is WsEvent.MessageToken -> {
|
||||
// The interrupted partial is already sealed. Wait for its authoritative
|
||||
// completion instead of letting trailing deltas create a second stream.
|
||||
if (_streamingState.value.interruptedMessage != null && isCurrentSession(event.sessionId)) return
|
||||
if (isCurrentSession(event.sessionId)) mainTurnBusy = true
|
||||
streamingController.handleMessageToken(event)
|
||||
}
|
||||
@@ -2019,9 +2022,23 @@ class ChatViewModel(
|
||||
if (targetsCurrentTurn && status in setOf("interrupted", "not_interrupted")) {
|
||||
// Never let a late result for an older same-session turn
|
||||
// erase a newer stream or make its replacement race it.
|
||||
streamingController.flushPendingTransition()
|
||||
val interruptedStream = _streamingState.value
|
||||
sealStreamingMessageIfAny()
|
||||
val interruptedMessage =
|
||||
interruptedStream.streamingMessage?.let { stream ->
|
||||
_uiState.value.messages.firstOrNull { it.id == stream.id }
|
||||
}
|
||||
_uiState.update { it.copy(isAgentTyping = false) }
|
||||
_streamingState.update { StreamingState() }
|
||||
_streamingState.update {
|
||||
interruptedStream.copy(
|
||||
streamingMessage = null,
|
||||
interruptedMessage = interruptedMessage,
|
||||
isThinking = false,
|
||||
thinkingText = "",
|
||||
isReasoning = false,
|
||||
)
|
||||
}
|
||||
streamingController.resetStreaming()
|
||||
if (status == "interrupted") addSystemMessage("Session interrupted")
|
||||
mainTurnBusy = false
|
||||
|
||||
@@ -457,7 +457,7 @@ object ChatWsEventReducer {
|
||||
val finalSnapshot = event.rawPayload?.let(::parseUsageSnapshot)
|
||||
val usageState = finalSnapshot?.let { applyUsageSnapshot(state, it) } ?: state
|
||||
val turnUsage = finalSnapshot?.deltaFrom(streamingState.turnUsageBaseline)
|
||||
val streaming = streamingState.streamingMessage
|
||||
val streaming = streamingState.interruptedMessage ?: streamingState.streamingMessage
|
||||
// Prefer the authoritative reasoning carried in the message.complete
|
||||
// payload (the gateway's assembled full trace), then fall back to
|
||||
// whatever reasoning.delta deltas accumulated during streaming.
|
||||
@@ -663,7 +663,7 @@ object ChatWsEventReducer {
|
||||
streamingState.streamingMessage
|
||||
?: return ReducerResult(
|
||||
state = state.copy(isAgentTyping = false),
|
||||
streamingState = StreamingState(),
|
||||
streamingState = streamingState.takeIf { it.interruptedMessage != null } ?: StreamingState(),
|
||||
)
|
||||
val reasoning =
|
||||
if (streamingState.reasoningText.isNotBlank()) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.m57.hermescontrol.data.model.UsageSnapshotResponse
|
||||
|
||||
data class StreamingState(
|
||||
val streamingMessage: ChatMessage? = null,
|
||||
/** Sealed by an interrupt acknowledgement; late completion must update this row, not append another. */
|
||||
val interruptedMessage: ChatMessage? = null,
|
||||
val isThinking: Boolean = false,
|
||||
val thinkingText: String = "",
|
||||
val isReasoning: Boolean = false,
|
||||
|
||||
Reference in new issue
Block a user