refactor(chat): tag local feedback with displayKind and refine logical message matching (#1241)
This commit is contained in:
1 parent
c9bbadf5d5
commit
9ae7b6a520
5 files changed
+85
-25
No files matched your search
@@ -169,9 +169,21 @@ internal class TranscriptComparison(
|
||||
val key = toolKey(a.content)
|
||||
return key != null && key == toolKey(b.content)
|
||||
}
|
||||
// Rows generated only by Mobile never have a canonical REST counterpart.
|
||||
// Do not let coincidentally identical server text claim their identity.
|
||||
if (a.role == MessageRole.ASSISTANT &&
|
||||
(a.displayKind == "local_feedback" || b.displayKind == "local_feedback")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (a.role == MessageRole.USER &&
|
||||
(a.displayKind == "clarify_response" || b.displayKind == "clarify_response")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
val ta = trimmed.getOrPut(a.content) { a.content.trim() }
|
||||
val tb = trimmed.getOrPut(b.content) { b.content.trim() }
|
||||
if (ta.startsWith("/") || tb.startsWith("/")) return false
|
||||
if (a.role == MessageRole.USER && (ta.startsWith("/") || tb.startsWith("/"))) return false
|
||||
if (ta == tb) return true
|
||||
if (a.role == MessageRole.USER &&
|
||||
captions.getOrPut(ta) { stripAttachmentRefLines(ta) } ==
|
||||
@@ -458,27 +470,7 @@ private fun List<ChatMessage>.inTranscriptOrder(
|
||||
internal fun ChatMessage.isPermanentlyLocal(): Boolean =
|
||||
role == MessageRole.SYSTEM ||
|
||||
(role == MessageRole.USER && (content.startsWith("/") || displayKind == "clarify_response")) ||
|
||||
(role == MessageRole.ASSISTANT && canonicalRestId == null && isLocalFeedbackRow(this))
|
||||
|
||||
private fun isLocalFeedbackRow(message: ChatMessage): Boolean =
|
||||
message.localOrder != null || isLocalFeedbackContent(message.content)
|
||||
|
||||
private fun isLocalFeedbackContent(content: String): Boolean =
|
||||
content.endsWith(" is not supported on mobile") ||
|
||||
content.startsWith("usage: /queue") ||
|
||||
content.startsWith("reasoning: ") ||
|
||||
content.startsWith("Could not read reasoning status:") ||
|
||||
content == "Reasoning is not supported for the current model." ||
|
||||
content == "Reasoning cannot be disabled for this model (always on)." ||
|
||||
content.startsWith("Could not change reasoning:") ||
|
||||
content == "Reasoning controls require an active session." ||
|
||||
content == "No active session. Use `/new` to create one." ||
|
||||
content == "No active session to undo." ||
|
||||
content == "No active session for side questions. Start a chat first." ||
|
||||
content.startsWith("Bot chats are one continuous conversation — compacting instead.") ||
|
||||
content.startsWith("⚠ ") ||
|
||||
(content.startsWith("/") && content.contains(": ")) ||
|
||||
content == "Failed to undo."
|
||||
(role == MessageRole.ASSISTANT && displayKind == "local_feedback")
|
||||
|
||||
internal fun ChatMessage.isSessionStartMarker(): Boolean =
|
||||
role == MessageRole.SYSTEM && (content == "Session created" || content == "Session branched")
|
||||
|
||||
@@ -2655,7 +2655,12 @@ class ChatViewModel(
|
||||
}
|
||||
|
||||
private fun addAssistantMessage(text: String) {
|
||||
val msg = ChatMessage(role = MessageRole.ASSISTANT, content = text)
|
||||
val msg =
|
||||
ChatMessage(
|
||||
role = MessageRole.ASSISTANT,
|
||||
content = text,
|
||||
displayKind = "local_feedback",
|
||||
)
|
||||
_uiState.update { it.copy(messages = it.messages + msg) }
|
||||
|
||||
// Persist — OUTSIDE update{}
|
||||
|
||||
@@ -58,7 +58,10 @@ private fun ChatMessage.isSyntheticSystemRow(): Boolean =
|
||||
content.startsWith(MAX_ITERATIONS_SYSTEM_MARKER)
|
||||
|
||||
internal fun ChatMessage.isTimelineMarker(): Boolean =
|
||||
displayKind != null && displayKind != "steer" && displayKind != "clarify_response"
|
||||
displayKind != null &&
|
||||
displayKind != "steer" &&
|
||||
displayKind != "clarify_response" &&
|
||||
displayKind != "local_feedback"
|
||||
|
||||
/**
|
||||
* Split a flat message list into turns for the full-bleed renderer.
|
||||
|
||||
@@ -581,6 +581,51 @@ class ChatPagingMergeTest {
|
||||
assertTrue(sameLogicalMessage(canonical.copy(reasoningText = "new trace"), live))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assistantReplyBeginningWithSlashStillMatchesCanonicalEcho() {
|
||||
val live =
|
||||
ChatMessage(
|
||||
id = "uuid-answer",
|
||||
role = MessageRole.ASSISTANT,
|
||||
content = "/help is the command you're looking for.",
|
||||
)
|
||||
val canonical =
|
||||
live.copy(
|
||||
id = "rest-session-10",
|
||||
restId = null,
|
||||
)
|
||||
|
||||
assertTrue(sameLogicalMessage(canonical, live))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun permanentlyLocalRowsDoNotMatchCanonicalRowsByContent() {
|
||||
val localFeedback =
|
||||
ChatMessage(
|
||||
id = "uuid-feedback",
|
||||
role = MessageRole.ASSISTANT,
|
||||
content = "Command completed successfully",
|
||||
displayKind = "local_feedback",
|
||||
)
|
||||
val canonicalAssistant =
|
||||
ChatMessage(
|
||||
id = "rest-session-10",
|
||||
role = MessageRole.ASSISTANT,
|
||||
content = "Command completed successfully",
|
||||
)
|
||||
val clarify =
|
||||
ChatMessage(
|
||||
id = "uuid-clarify",
|
||||
role = MessageRole.USER,
|
||||
content = "yes",
|
||||
displayKind = "clarify_response",
|
||||
)
|
||||
val canonicalUser = ChatMessage(id = "rest-session-11", role = MessageRole.USER, content = "yes")
|
||||
|
||||
assertTrue(!sameLogicalMessage(canonicalAssistant, localFeedback))
|
||||
assertTrue(!sameLogicalMessage(canonicalUser, clarify))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stripGatewaySteerWrapperRemovesEnvelopesSafely() {
|
||||
val standard =
|
||||
@@ -809,7 +854,12 @@ class ChatPagingMergeTest {
|
||||
val answer = ChatMessage(id = "rest-session-1", role = MessageRole.ASSISTANT, content = "I am Hermes.")
|
||||
val command = ChatMessage(id = "uuid-cmd", role = MessageRole.USER, content = "/reasoning")
|
||||
val output =
|
||||
ChatMessage(id = "uuid-output", role = MessageRole.ASSISTANT, content = "reasoning: low · display on")
|
||||
ChatMessage(
|
||||
id = "uuid-output",
|
||||
role = MessageRole.ASSISTANT,
|
||||
content = "Command completed successfully",
|
||||
displayKind = "local_feedback",
|
||||
)
|
||||
|
||||
val current = listOf(prompt, answer, command, output)
|
||||
|
||||
|
||||
@@ -103,6 +103,16 @@ class FullBleedTurnsTest {
|
||||
assertEquals(listOf<ChatTurn>(ChatTurn.User(steer)), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `local feedback display kind remains assistant prose`() {
|
||||
val feedback =
|
||||
msg("a1", MessageRole.ASSISTANT, content = "Command completed successfully")
|
||||
.copy(displayKind = "local_feedback")
|
||||
val result = groupIntoTurns(listOf(feedback))
|
||||
|
||||
assertEquals(listOf<ChatTurn>(entries(AgentEntry.Prose(feedback))), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `max-iterations nudge is a SystemEvent not a user bubble`() {
|
||||
// Backend persists the max-iterations notice as a plain role=user row
|
||||
|
||||
Reference in new issue
Block a user