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