Files
ai-app/app/androidApp/src/main/kotlin/com/example/aiapp/Compaction.kt
T
iris 404066fa7d Say when a session is working, and what it was told
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.
2026-08-29 15:27:46 -04:00

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"
}