Three things a phone could not see, all of them the same shape: the session was doing something and nothing on screen said so. A turn nobody here started never reported itself. `Running` was sent where a message was *sent*, so a session picked up mid-turn, one compacting on its own, or one another agent wrote to sat there reading as idle until it finished. The driver now says it from what it observes -- output that could only come from a turn in flight -- which is the same set of events that already announced a steer, with the ends swapped. An imported session had it worse: nothing but replayed lines ever reaches it, and a status was not among them, so it was permanently whatever it was when it was adopted. Its file does not record a turn ending, but it does record why each assistant message stopped, and `tool_use` versus anything else answers it. A record that says nothing leaves the status alone rather than voting for idle. Messages from other agents were dropped outright: the CLI marks them meta, and this replayed everything except meta. They are now a row of their own, closed by default like a tool call, named for the session that sent it -- not the reader's own bubble, because they did not say it, and a session working on something this phone never asked for is exactly what one of these explains. Measured against a real session file rather than guessed: the peer record carries the sender's name and the message body in `origin`, beside a copy wrapped for the model to read.
58 lines
2.4 KiB
Kotlin
58 lines
2.4 KiB
Kotlin
package com.example.aiapp
|
|
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.material3.Card
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
/**
|
|
* A message another agent sent this session, closed until somebody asks.
|
|
*
|
|
* Closed by default, like a tool call and for the same reason: these are long, there can be several
|
|
* in a row, and what a reader scanning the transcript needs from one is that it happened and who
|
|
* sent it. The first line comes with the heading because a name alone does not say which message
|
|
* this was.
|
|
*
|
|
* Drawn as its own kind rather than as the reader's own bubble. They did not say this, and a
|
|
* transcript that puts it in their voice is making a claim about who asked for the work that
|
|
* follows -- which is exactly the question a peer message is usually the answer to.
|
|
*/
|
|
@Composable
|
|
fun PeerMessageRow(
|
|
item: TranscriptItem.PeerNote,
|
|
expanded: Boolean,
|
|
onToggle: () -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
Card(modifier.fillMaxWidth().clickable(onClick = onToggle)) {
|
|
Column(Modifier.padding(12.dp)) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text("Message from ${item.from}", style = MaterialTheme.typography.titleSmall)
|
|
if (!expanded) {
|
|
Spacer(Modifier.width(8.dp))
|
|
Text(
|
|
item.text.lineSequence().firstOrNull { it.isNotBlank() }.orEmpty(),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
maxLines = 1,
|
|
// The head, not the tail: a message is identified by how it opens.
|
|
overflow = TextOverflow.Ellipsis,
|
|
)
|
|
}
|
|
}
|
|
if (expanded) MarkdownText(item.text, Modifier.padding(top = 6.dp))
|
|
}
|
|
}
|
|
}
|