Files
ai-app/app/androidApp/src/main/kotlin/com/example/aiapp/Commands.kt
T
irisandClaude Opus 5 edc39c7371 Thin the app's comments
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>
2026-09-04 16:20:16 -04:00

152 lines
6.2 KiB
Kotlin

package com.example.aiapp
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
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.unit.dp
/**
* Something a session can be asked to do to itself, rather than something to say to it.
*
* These are the two this app understands, and understanding them is what lets it show them: a
* suggestion while one is being typed, a name in the settings screen that sends one, and a bubble
* that stays up while the session is too busy to run it. Anything else beginning with "/" is passed
* through, because a dialect's own vocabulary grows without this list.
*/
data class SessionCommand(
/** With the slash, as it is typed and as it is sent. */
val name: String,
/** One line, in the suggestion list: what it does, not how. */
val summary: String,
/** What follows the name, named for the reader, or null when nothing does. */
val argument: String?,
) {
/** What to put in the box when this is picked: ready to send, or ready to be finished. */
fun typed(): String = if (argument == null) name else "$name "
}
val SESSION_COMMANDS =
listOf(
SessionCommand(
"/compact",
"Summarise the conversation so far and carry on from the summary",
null,
),
SessionCommand(
"/clear",
"Start fresh: drop the conversation from the session's context, keeping it on screen",
null,
),
SessionCommand("/rename", "Change what this session is called", "name"),
)
/**
* The commands worth offering for what has been typed so far.
*
* Only for a line that starts with a slash and has not yet become a whole command with an argument
* -- once there is something after "/rename ", the reader is writing the name and a list of
* commands underneath it is in the way.
*/
fun suggestedCommands(input: String): List<SessionCommand> {
if (!input.startsWith("/") || input.contains(' ')) return emptyList()
return SESSION_COMMANDS.filter { it.name.startsWith(input) }
}
/**
* The commands matching what is being typed, above the box they are being typed into.
*
* Above rather than over: a list that covers the transcript hides what the command is about, and
* the reader is usually looking at the thing they mean to act on.
*/
@Composable
fun CommandSuggestions(
commands: List<SessionCommand>,
onPick: (SessionCommand) -> Unit,
modifier: Modifier = Modifier,
) {
if (commands.isEmpty()) return
Card(modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
Column(Modifier.padding(vertical = 4.dp)) {
commands.forEach { command ->
Row(
Modifier.fillMaxWidth()
.clickable { onPick(command) }
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
// The command in the colour commands are, so the suggestion and the bubble
// it becomes are visibly the same thing.
if (command.argument == null) command.name
else "${command.name} <${command.argument}>",
style = MaterialTheme.typography.titleSmall,
color = commandColor,
)
Spacer(Modifier.width(12.dp))
Text(
command.summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
}
/**
* A command, where the reader put it: at their end of the conversation.
*
* Blue rather than the colour of something they said, because they did not say it to the model --
* it is an instruction to the session, and the reply to it is the session changing rather than
* anything appearing here.
*
* [waiting] is a command the session is too busy to run yet, which is a state with a spinner and a
* reason: pressing Compact in the middle of a long turn otherwise does nothing visible for minutes.
*/
@Composable
fun CommandBubble(text: String, waiting: Boolean = false) {
Box(Modifier.fillMaxWidth()) {
Card(
colors = CardDefaults.cardColors(containerColor = commandColor),
modifier = Modifier.align(Alignment.CenterEnd).padding(start = 48.dp),
) {
Column(Modifier.padding(12.dp)) {
// Stated beside the fill rather than inherited: a semantic colour has to carry its
// own contrast, because the surface under it will not change to rescue it.
Text(text, color = MaterialTheme.colorScheme.inverseOnSurface)
if (waiting) {
Spacer(Modifier.height(6.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(
modifier = Modifier.width(12.dp).height(12.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.inverseOnSurface,
)
Spacer(Modifier.width(6.dp))
Text(
"waiting for this turn to end",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.inverseOnSurface,
)
}
}
}
}
}
}