Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
a49120b0c8
9 files changed
+177
-48
No files matched your search
@@ -20,7 +20,28 @@ data class SeqEvent(val seq: Long, val ts: Double, val event: SessionEvent)
|
||||
data class QuestionOption(val label: String, val description: String?, val preview: String?)
|
||||
|
||||
sealed class SessionEvent {
|
||||
data class UserMessage(val text: String) : SessionEvent()
|
||||
data class UserMessage(
|
||||
val text: String,
|
||||
/**
|
||||
* The [MessageQueued] this resolves, or null when it never waited.
|
||||
*
|
||||
* Matched on rather than the text, because the same message sent twice is two waiting
|
||||
* bubbles and clearing whichever one matched first would leave the wrong one on screen.
|
||||
*/
|
||||
val id: String?,
|
||||
) : SessionEvent()
|
||||
|
||||
/**
|
||||
* A message the server has accepted and the session has not read yet.
|
||||
*
|
||||
* From the server, not from this app's memory of what it sent. The pending bubble used to be
|
||||
* screen state, so leaving the session or restarting the app drew nothing waiting while the
|
||||
* message was still queued -- and nothing waiting is what "there is nothing" looks like.
|
||||
*
|
||||
* Resolved by the [UserMessage] carrying the same id, exactly as [CommandQueued] is resolved by
|
||||
* [CommandSent].
|
||||
*/
|
||||
data class MessageQueued(val id: String, val text: String) : SessionEvent()
|
||||
|
||||
data class AssistantText(val delta: String) : SessionEvent()
|
||||
|
||||
@@ -119,7 +140,13 @@ fun parseSeqEvent(json: String): SeqEvent {
|
||||
val body = JSONObject(json)
|
||||
val event =
|
||||
when (val type = body.getString("type")) {
|
||||
"userMessage" -> SessionEvent.UserMessage(body.getString("text"))
|
||||
"userMessage" ->
|
||||
SessionEvent.UserMessage(
|
||||
body.getString("text"),
|
||||
body.optString("id").ifEmpty { null },
|
||||
)
|
||||
"messageQueued" ->
|
||||
SessionEvent.MessageQueued(body.getString("id"), body.getString("text"))
|
||||
"assistantText" -> SessionEvent.AssistantText(body.getString("delta"))
|
||||
"toolStart" ->
|
||||
SessionEvent.ToolStart(
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user