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>
152 lines
6.2 KiB
Kotlin
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,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|