The same pass the server had, on the Kotlin side: comments restating what the code says are gone, and the ones recording a measurement, a constraint or an incident are kept but cut to a few lines each. 6540 comment lines to 5674, and 920 lines off the app. Two doc comments had drifted onto the item above the one they describe -- `contextAfter`'s onto `sessionWorking` in Events.kt, and `UsageMonitor`'s equivalent on the server was fixed in the previous commit. Each is back on its own item, which is the only non-comment line this diff moves. The comments are reflowed to the column limit at their own indentation: several were written wide, and ktfmt re-wrapped them into lines holding a single orphan word. `/tmp` script, not kept -- ktfmt is idempotent over the result, which is the check. Left alone deliberately: this codebase's remaining comment density is high because the comments carry things the code cannot say -- what a null means, what a number was measured against, which bug a guard exists for. Of the 238 one-line doc comments in the app, five were pure restatement of the name and were removed; the rest each say something the signature does not. ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest pass; cargo test (127), clippy --all-targets and fmt still clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
5.9 KiB
Kotlin
142 lines
5.9 KiB
Kotlin
package com.example.aiapp
|
|
|
|
import androidx.compose.foundation.background
|
|
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.foundation.shape.CornerSize
|
|
import androidx.compose.material3.CardDefaults
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.CompositionLocalProvider
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Color
|
|
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.
|
|
*
|
|
* Opened, the card is drawn in *pieces* -- this heading and one [PeerBlockRow] per markdown block,
|
|
* each its own item of the transcript list. See [TranscriptUnit.PeerHead] for what that bought;
|
|
* what matters here is that the pieces have to add up to the card that was there before, so the
|
|
* fill, the corner radius and the padding all live in [peerSurface].
|
|
*/
|
|
@Composable
|
|
fun PeerHeadRow(
|
|
item: TranscriptItem.PeerNote,
|
|
open: Boolean,
|
|
onToggle: () -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
Column(
|
|
modifier.cardPiece(
|
|
top = true,
|
|
bottom = !open,
|
|
fill = CardDefaults.cardColors().containerColor,
|
|
onPress = onToggle,
|
|
)
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text("Message from ${item.from}", style = MaterialTheme.typography.titleSmall)
|
|
if (!open) {
|
|
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,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One block of an opened peer message, on the same card the heading started.
|
|
*
|
|
* Clickable like the heading, so the card still shuts wherever it is pressed -- it was one control
|
|
* before it was several items, and which piece the finger lands on is not something the reader
|
|
* chose.
|
|
*/
|
|
@Composable
|
|
fun PeerBlockRow(unit: TranscriptUnit.PeerBlock, replies: ParsedReplies, onToggle: () -> Unit) {
|
|
Column(
|
|
Modifier.cardPiece(
|
|
top = false,
|
|
bottom = unit.last,
|
|
fill = CardDefaults.cardColors().containerColor,
|
|
onPress = onToggle,
|
|
)
|
|
) {
|
|
// The words shut the card too, and have to do it themselves -- see [LocalMarkdownTap].
|
|
// Without this the card closes everywhere except on the text, which is most of it.
|
|
CompositionLocalProvider(LocalMarkdownTap provides rememberMarkdownTap(onToggle)) {
|
|
// The gap the card's own column used to provide between its heading and its prose, and
|
|
// between one block and the next -- inside the piece, so the card's fill runs through
|
|
// it.
|
|
MarkdownPiece(unit.text, unit.piece, replies, Modifier.padding(top = unit.spacing))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One piece of a card drawn in slices: the fill, the corners it owns, and the room inside it.
|
|
*
|
|
* A filled Material card is elevation zero, so there is no shadow that a seam would show through --
|
|
* which is the whole reason a card can be cut up at all. Each piece paints the caller's container
|
|
* colour and rounds only the corners at the ends of the message, so the pieces abut into one
|
|
* continuous card. Shared by the two rows cut this way -- an opened peer message and a long user
|
|
* message -- because two copies of the corner logic is how one of them grows a seam.
|
|
*
|
|
* The padding is the other half: 12dp all round was the card's own, so the top piece keeps the top
|
|
* of it, the bottom piece the bottom, and the middle pieces neither.
|
|
*/
|
|
@Composable
|
|
fun Modifier.cardPiece(
|
|
top: Boolean,
|
|
bottom: Boolean,
|
|
fill: Color,
|
|
onPress: (() -> Unit)? = null,
|
|
): Modifier {
|
|
val square = CornerSize(0.dp)
|
|
val shape =
|
|
MaterialTheme.shapes.medium.copy(
|
|
topStart = if (top) MaterialTheme.shapes.medium.topStart else square,
|
|
topEnd = if (top) MaterialTheme.shapes.medium.topEnd else square,
|
|
bottomStart = if (bottom) MaterialTheme.shapes.medium.bottomStart else square,
|
|
bottomEnd = if (bottom) MaterialTheme.shapes.medium.bottomEnd else square,
|
|
)
|
|
return fillMaxWidth()
|
|
.clip(shape)
|
|
.background(fill)
|
|
.then(if (onPress == null) Modifier else Modifier.clickable(onClick = onPress))
|
|
.padding(
|
|
start = CARD_PADDING,
|
|
end = CARD_PADDING,
|
|
top = if (top) CARD_PADDING else 0.dp,
|
|
bottom = if (bottom) CARD_PADDING else 0.dp,
|
|
)
|
|
}
|
|
|
|
/** The room inside a sliced card, which was `Card { Column(padding(12.dp)) }`. */
|
|
private val CARD_PADDING = 12.dp
|