Files
ai-app/app/androidApp/src/main/kotlin/com/example/aiapp/ToolInput.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

138 lines
5.5 KiB
Kotlin

package com.example.aiapp
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import org.json.JSONObject
/**
* A tool call's input, read rather than dumped.
*
* Every tool's input arrives as JSON, and showing it raw makes the reader parse
* `{"command":"…","timeout":120000}` themselves to find the one line they care about. So the fields
* that carry the meaning are pulled out, and anything left over is still shown, because dropping a
* field would be claiming the tool has no other input when it might.
*/
data class ToolInput(
/** The thing that will actually be run or read, if this tool has one. */
val subject: String?,
/** The language [subject] is written in, for highlighting. */
val language: Language?,
/** The tool's own one-line summary, when it wrote one. */
val description: String?,
/**
* How long the call may take, in the largest units it fits. Shown apart because it is a limit
* on the call rather than part of what the call does.
*/
val timeout: String?,
/** Everything else, as `name: value` lines. Never dropped. */
val rest: List<String>,
) {
/** The one line to show when there is only room for one: what this call is for. */
val title: String?
get() = description ?: subject
}
/**
* Which field of which tool is the subject.
*
* A table rather than a chain of `if`s: adding a tool is a row, and the shape stops any of them
* from being the special case that gets its own code path. Unknown tools fall through to "no
* subject, everything is rest".
*/
private val SUBJECTS: Map<String, Pair<String, Language?>> =
mapOf(
"Bash" to ("command" to Language.SHELL),
"Read" to ("file_path" to null),
"Write" to ("file_path" to null),
"Edit" to ("file_path" to null),
"Glob" to ("pattern" to null),
"Grep" to ("pattern" to null),
"WebFetch" to ("url" to null),
)
/** Fields that are the tool's own prose about itself rather than input to it. */
private val DESCRIPTIONS = listOf("description", "prompt")
fun parseToolInput(tool: String, input: String): ToolInput {
val json =
try {
JSONObject(input)
} catch (_: org.json.JSONException) {
// Not an object: older transcripts and some tools send a bare string. It is still the
// input, so it is still shown.
return ToolInput(
null,
null,
null,
null,
input.takeIf { it.isNotBlank() }?.let { listOf(it) }.orEmpty(),
)
}
val (subjectKey, language) = SUBJECTS[tool] ?: (null to null)
val subject = subjectKey?.let { json.optString(it) }?.takeIf { it.isNotBlank() }
val description = DESCRIPTIONS.firstNotNullOfOrNull {
json.optString(it).takeIf { v -> v.isNotBlank() }
}
val timeout = json.optString("timeout").takeIf { it.isNotBlank() }?.let { formatMillisText(it) }
val rest =
json
.keys()
.asSequence()
.filter { it != subjectKey || subject == null }
.filter { it !in DESCRIPTIONS || description == null }
.filter { it != "timeout" || timeout == null }
.sorted()
.map { key -> "$key: ${json.get(key)}" }
.toList()
return ToolInput(subject, language, description, timeout, rest)
}
/**
* A tool call's input: its subject highlighted, then whatever else it carried.
*
* On the dark surface every verbatim thing in the app sits on. Drawn as nothing at all when the
* call carried neither, rather than as an empty block: a tinted rectangle with nothing in it is a
* rendering fault.
*
* The description is *not* here. It is the tool's own prose about what it is doing, so it belongs
* with the reader's text rather than inside the machine's; [ToolCard] draws it above this.
*/
@Composable
fun ToolInputView(tool: String, input: String, modifier: Modifier = Modifier) {
val parsed = remember(tool, input) { parseToolInput(tool, input) }
if (parsed.subject == null && parsed.rest.isEmpty()) return
RawBlock(modifier) {
parsed.subject?.let { subject ->
// Not wrapped: a wrapped command hides where its arguments end, and the long one is the
// one being read closely.
Text(
// Not cached: a tool's subject is one command line, which lexes in microseconds --
// the cache exists for a fence with two hundred lines in it.
remember(subject, parsed.language) { highlight(subject, parsed.language) },
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
softWrap = false,
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
)
}
parsed.rest.forEach {
Text(
it,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp),
)
}
}
}