Merge remote-tracking branch 'origin/main'

This commit is contained in:
iris committed 2026-08-29 22:45:22 -04:00
commit a49120b0c8
9 files changed
+177 -48

No files matched your search

@@ -269,6 +269,9 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
is SessionEvent.CommandSent -> items + TranscriptItem.CommandRow(entry.seq, event.text)
// Screen-level state, not transcript rows -- see SessionScreen.
is SessionEvent.CommandQueued -> items
// No row of its own: a message that is still waiting is drawn as a pending bubble below
// the transcript, and becomes an ordinary one where the session read it.
is SessionEvent.MessageQueued -> items
is SessionEvent.Settings -> items
is SessionEvent.Status -> items
is SessionEvent.Error -> items + TranscriptItem.ErrorMsg(entry.seq, event.message)
@@ -373,7 +376,11 @@ fun SessionScreen(
// the working indicator, because that is where it is in the session's
// reading of events: after everything taken in, not yet taken in
// itself.
var queued by remember { mutableStateOf(listOf<String>()) }
// Messages the server has taken and the session has not read yet, by the id that will resolve
// them. From the event stream rather than from what this screen sent, so they are still here
// after leaving the session or restarting the app -- and so a message sent from another device
// is drawn waiting on this one too.
var queued by remember { mutableStateOf(listOf<Pair<String, String>>()) }
// Commands the session has been asked to run and cannot yet, by the id that will resolve
// them. From the server rather than from this screen, so a rename sent from the settings
// screen -- or from another device -- is drawn waiting here too.
@@ -426,7 +433,14 @@ fun SessionScreen(
// all that distinguishes one message from an identical
// earlier one -- and only the first match, so two
// identical messages wait twice.
if (event is SessionEvent.UserMessage) queued = queued - event.text
// Waiting, then read. Matched by id: the same message sent twice is two
// bubbles, and clearing by text would take away whichever matched first.
if (event is SessionEvent.MessageQueued) {
queued = queued + (event.id to event.text)
}
if (event is SessionEvent.UserMessage) {
queued = queued.filterNot { it.first == event.id }
}
// Waiting, then gone: a command leaves this list when the session takes it,
// and the row it becomes is added by `foldEvent` in the same pass.
if (event is SessionEvent.CommandQueued) {
@@ -689,18 +703,11 @@ fun SessionScreen(
input = ""
saveDraft(context, summary.id, "")
pendingAttachments = emptyList()
if (running && text.isNotEmpty()) queued = queued + text
// A held message leaves this list exactly two ways: the session
// reads it, which comes back as a UserMessage (see `apply`), or the
// send itself failed and there is nothing to wait for. Clearing the
// whole list when a turn ended was neither -- the server holds a
// queue of its own and takes one message per turn, so ending a turn
// is precisely when the *rest* are still waiting. It wiped them off
// the screen while they were on their way, which reads as messages
// two and three having been dropped.
act(onFailure = { queued = queued - text }) {
sendMessage(settings, summary.id, text, attachments)
}
// Nothing is added here. The server says what is waiting -- it emits `messageQueued`
// when it takes a message it cannot deliver yet -- and this screen draws that. Holding a
// local copy as well was the bug: the two agreed only until the app was restarted or the
// session left, and then the screen showed nothing pending while the queue was full.
act { sendMessage(settings, summary.id, text, attachments) }
}
// The system photo picker; the image uploads as soon as it's chosen,
@@ -827,7 +834,7 @@ fun SessionScreen(
waitingCommands.forEach { (_, text) ->
CommandBubble(text, waiting = true)
}
queued.forEach { text -> UserBubble(text, pending = true) }
queued.forEach { (_, text) -> UserBubble(text, pending = true) }
}
}
}