fix(chat): keep restored commands in place on session load (#1414)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 17:14:59 +03:00
1 parent 9f033ac82c
commit dcf62c0c02
2 files changed
+61

No files matched your search

@@ -539,6 +539,7 @@ private fun List<ChatMessage>.inTranscriptOrder(
val latestCanonical = mapNotNull { it.canonicalOrder }.maxOrNull() ?: -1L
// Session-start markers use -1 and must remain before unresolved cached history.
val beforeCanonical = (mapNotNull { it.canonicalOrder }.filter { it >= 0L }.minOrNull() ?: 0L) - 1L
val timedCanonical = mapNotNull { m -> m.canonicalOrder?.takeIf { it >= 0L }?.let { m.timestamp to it } }
var precedingCanonical: Long? = null
var hasPendingPredecessor = false
var pendingLocalOrder: Long? = null
@@ -550,6 +551,11 @@ private fun List<ChatMessage>.inTranscriptOrder(
precedingCanonical = order
hasPendingPredecessor = false
pendingLocalOrder = null
} else if (message.localOrder != null && message.isPermanentlyLocal()) {
// Restored from Room, where local rows sort after every server row. Seat it by time after the
// last confirmed row that is not newer, or before the loaded window when it predates it.
localAnchors[message.id] =
timedCanonical.filter { it.first <= message.timestamp }.maxOfOrNull { it.second } ?: beforeCanonical
} else if (message.isPermanentlyLocal()) {
localAnchors[message.id] =
if (hasPendingPredecessor) {
@@ -1209,4 +1209,59 @@ class ChatPagingMergeTest {
chronological = !older,
preserveLiveIds = true,
)
@Test
fun restoredLocalCommandsKeepTheirChronologicalPlaceInCachedPage() {
fun row(
id: String,
role: MessageRole,
content: String,
ts: Long,
local: Long? = null,
) = ChatMessage(id = id, role = role, content = content, timestamp = ts, localOrder = local)
// Room order: confirmed rows first, then every local row (sort_group 1).
val cachedPage =
listOf(
row("rest-s-1", MessageRole.USER, "hi", 10L),
row("rest-s-2", MessageRole.ASSISTANT, "hello", 11L),
row("rest-s-3", MessageRole.USER, "more", 30L),
row("rest-s-4", MessageRole.ASSISTANT, "ok", 31L),
row("rest-s-5", MessageRole.USER, "again", 50L),
row("rest-s-6", MessageRole.ASSISTANT, "sure", 51L),
row("cmd-a", MessageRole.USER, "/help", 12L, local = 1L),
row("cmd-b", MessageRole.USER, "/usage", 32L, local = 2L),
row("cmd-c", MessageRole.USER, "/model", 52L, local = 3L),
)
val merged = mergeCachedTranscriptPage(cachedPage, emptyList())
assertEquals(
listOf("rest-s-1", "rest-s-2", "cmd-a", "rest-s-3", "rest-s-4", "cmd-b", "rest-s-5", "rest-s-6", "cmd-c"),
merged.map { it.id },
)
}
@Test
fun restoredCommandOlderThanLoadedWindowStaysAboveItNotAtTheTail() {
val server =
listOf(
ChatMessage(id = "rest-s-50", role = MessageRole.USER, content = "a", timestamp = 500L),
ChatMessage(id = "rest-s-51", role = MessageRole.ASSISTANT, content = "b", timestamp = 501L),
)
val old =
ChatMessage(
id = "cmd",
role = MessageRole.USER,
content = "/model x",
timestamp = 100L,
localOrder = 9L,
)
val merged = mergeCachedTranscriptPage(server + old, emptyList())
assertEquals(listOf("cmd", "rest-s-50", "rest-s-51"), merged.map { it.id })
// Stable when an older page arrives afterwards.
val older = ChatMessage(id = "rest-s-10", role = MessageRole.USER, content = "o", timestamp = 50L)
val olderMid = ChatMessage(id = "rest-s-11", role = MessageRole.ASSISTANT, content = "p", timestamp = 150L)
val more = mergeCachedTranscriptPage(listOf(older, olderMid), merged)
assertEquals(listOf("rest-s-10", "cmd", "rest-s-11", "rest-s-50", "rest-s-51"), more.map { it.id })
}
}