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.
109 lines
4.9 KiB
Kotlin
109 lines
4.9 KiB
Kotlin
package com.example.aiapp
|
|
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.material3.LinearProgressIndicator
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
/**
|
|
* The mark a compaction leaves in the transcript.
|
|
*
|
|
* Centred and dim, because it is a divider rather than something anybody said: everything above it
|
|
* is out of the session's context now, and that is a fact about the conversation, not a turn in it.
|
|
* It has no collapsed form -- it is already one line, and there is nothing behind it to open.
|
|
*/
|
|
@Composable
|
|
fun CompactedRow(item: TranscriptItem.CompactedNote, modifier: Modifier = Modifier) {
|
|
Text(
|
|
compactionSummary(item),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
textAlign = TextAlign.Center,
|
|
modifier = modifier.fillMaxWidth().padding(vertical = 8.dp),
|
|
)
|
|
}
|
|
|
|
/**
|
|
* What to say about a compaction, given what was measured about it.
|
|
*
|
|
* The counts are the whole point when they are there -- "a million tokens became ten thousand" is
|
|
* the reader's answer to why the wait was worth it. When they are not, this says nothing about size
|
|
* rather than filling in a plausible number, and when only the size before is known it says exactly
|
|
* that much.
|
|
*
|
|
* `auto` is named because it is the case the reader did not ask for, and so the one that explains a
|
|
* session going quiet on its own. Anything else -- including a trigger this build does not
|
|
* recognise -- makes no claim about who asked, which is the honest reading of not knowing.
|
|
*/
|
|
fun compactionSummary(item: TranscriptItem.CompactedNote): String {
|
|
val what = if (item.trigger == "auto") "Compacted automatically" else "Compacted"
|
|
val pre = item.preTokens
|
|
val post = item.postTokens
|
|
return when {
|
|
pre != null && post != null -> "$what -- ${tokens(pre)} to ${tokens(post)} tokens"
|
|
pre != null -> "$what -- was ${tokens(pre)} tokens, new size not reported"
|
|
else -> what
|
|
}
|
|
}
|
|
|
|
private fun tokens(count: Long): String = "%,d".format(count)
|
|
|
|
/**
|
|
* Where the working indicator goes while a compaction is running.
|
|
*
|
|
* A bar across the whole row rather than the spinner an ordinary turn gets, because a compaction is
|
|
* not an ordinary turn: nothing arrives in the transcript while it runs, so the row it occupies is
|
|
* the only thing on screen that is moving, and at the width of a spinner that reads as a session
|
|
* that might have hung.
|
|
*
|
|
* The bar is indeterminate, and that is a statement rather than an omission. The CLI says a
|
|
* compaction has started and then says nothing at all until it has finished -- measured, not
|
|
* assumed -- so there is no fraction to fill, and a bar that crept along at the pace of the last
|
|
* compaction would be this screen inventing the part nobody sent it. What it can honestly say is
|
|
* that work is happening and for how long, which is [compactingLabel].
|
|
*/
|
|
@Composable
|
|
fun CompactingRow(seconds: Long?, modifier: Modifier = Modifier) {
|
|
Column(modifier.fillMaxWidth()) {
|
|
Text(
|
|
compactingLabel(seconds),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
// The colour is stated beside the fill rather than inherited: a semantic colour has
|
|
// to carry its own contrast, since the surface under it will not change to rescue it.
|
|
color = compactingColor,
|
|
)
|
|
LinearProgressIndicator(
|
|
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
|
|
color = compactingColor,
|
|
trackColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* What the working indicator says while a compaction is running.
|
|
*
|
|
* Elapsed time and nothing else, because elapsed time is all there is: the CLI announces that a
|
|
* compaction has begun and then says nothing until it has finished, so any bar, percentage or
|
|
* estimate here would be this screen's guess wearing a measurement's clothes. Knowing it has been
|
|
* going forty seconds is what a reader actually wants -- it is the difference between waiting and
|
|
* going to look at why.
|
|
*
|
|
* [seconds] is null when this device did not see the compaction start, which is what opening a
|
|
* session that is already compacting looks like. That case says only "compacting": no number is the
|
|
* honest answer, and a number counted from the moment the screen opened would be wrong in the
|
|
* direction that matters, since a compaction somebody is asking about is a long one.
|
|
*/
|
|
fun compactingLabel(seconds: Long?): String =
|
|
when {
|
|
seconds == null -> "compacting"
|
|
seconds < 60 -> "compacting ${seconds}s"
|
|
else -> "compacting ${seconds / 60}m ${seconds % 60}s"
|
|
}
|