Clarify subagent coordination cards

This commit is contained in:
iris committed 2026-09-13 02:48:21 -04:00
1 parent cad0cbcfbe
commit 898e6b92d0
5 files changed
+152 -21

No files matched your search

@@ -68,12 +68,14 @@ private val SUBJECTS: Map<String, Pair<String, Language?>> =
private val DESCRIPTIONS = listOf("description", "prompt")
fun parseToolInput(tool: String, input: String): ToolInput {
if (input.trim() == "null") return ToolInput(null, null, null, null, emptyList())
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.
// input, so it is still shown. JSON null is the one exception: it means the call had
// no input, and drawing the word makes an absent value look like an instruction.
return ToolInput(
null,
null,
@@ -85,17 +87,18 @@ fun parseToolInput(tool: String, input: String): ToolInput {
val (subjectKey, language) = SUBJECTS[tool] ?: (null to null)
val subject =
subjectKey
?.let { json.optString(it) }
?.let { json.text(it) }
?.takeIf { it.isNotBlank() }
?.let { if (tool == "Bash") renderedBashScript(it) ?: it else it }
val description = DESCRIPTIONS.firstNotNullOfOrNull {
json.optString(it).takeIf { v -> v.isNotBlank() }
json.text(it)?.takeIf { value -> value.isNotBlank() }
}
val timeout = json.optString("timeout").takeIf { it.isNotBlank() }?.let { formatMillisText(it) }
val timeout = json.text("timeout")?.takeIf { it.isNotBlank() }?.let { formatMillisText(it) }
val rest =
json
.keys()
.asSequence()
.filterNot(json::isNull)
.filter { it != subjectKey || subject == null }
.filter { it !in DESCRIPTIONS || description == null }
.filter { it != "timeout" || timeout == null }
@@ -105,6 +108,9 @@ fun parseToolInput(tool: String, input: String): ToolInput {
return ToolInput(subject, language, description, timeout, rest)
}
private fun JSONObject.text(key: String): String? =
if (isNull(key)) null else optString(key).takeIf { it.isNotEmpty() }
/**
* Removes Codex's rendered Bash argv from old transcript rows.
*
@@ -295,12 +295,14 @@ fun ToolCard(
shape: Shape = CardDefaults.shape,
) {
val parsed = remember(tool.tool, tool.input) { parseToolInput(tool.tool, tool.input) }
val name = toolDisplayName(tool.tool)
val output = toolDisplayOutput(tool.tool, tool.output)
val deciding = tool.asks.any { it.answers.isEmpty() }
val open = expanded || deciding
Card(Modifier.fillMaxWidth().clickable(onClick = onToggle), shape = shape) {
Column(Modifier.padding(GROUP_INSET_LARGE)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(tool.tool, style = MaterialTheme.typography.titleSmall)
Text(name, style = MaterialTheme.typography.titleSmall)
if (open) {
Spacer(Modifier.weight(1f))
parsed.timeout?.let {
@@ -355,7 +357,7 @@ fun ToolCard(
if (tool.tool != ASK_USER_QUESTION) {
ToolInputView(tool.tool, tool.input, Modifier.padding(top = 4.dp))
}
if (tool.output.isNotEmpty()) {
if (output.isNotEmpty()) {
Spacer(Modifier.height(8.dp))
Text("Output", style = MaterialTheme.typography.labelSmall)
// What the tool printed, on the surface everything verbatim gets and in the
@@ -367,7 +369,7 @@ fun ToolCard(
// often the whole of what a diff or a test run is saying. Remembered against
// the text, so a card that is open through a scroll parses once.
val palette = remember { ansiPalette() }
val styled = remember(tool.output, palette) { ansiStyled(tool.output, palette) }
val styled = remember(output, palette) { ansiStyled(output, palette) }
RawBlock(Modifier.padding(top = 2.dp)) {
Text(
styled,
@@ -391,6 +393,22 @@ fun ToolCard(
}
}
private val collaborationToolNames =
mapOf(
"Task" to "Spawn agent",
"TaskOutput" to "Wait for agents",
"SendMessage" to "Message agent",
"CloseAgent" to "Close agent",
"InterruptAgent" to "Interrupt agent",
"ListAgents" to "List agents",
"ResumeAgent" to "Resume agent",
)
internal fun toolDisplayName(tool: String): String = collaborationToolNames[tool] ?: tool
internal fun toolDisplayOutput(tool: String, output: String): String =
if (tool in collaborationToolNames && output == "completed") "" else output
/**
* The permission ask on the call it is about.
*
@@ -3,6 +3,7 @@ package com.example.aiapp
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ToolInputTest {
@Test
@@ -26,4 +27,17 @@ class ToolInputTest {
assertNull(renderedBashScript("/usr/bin/bash -lc echo hello"))
assertNull(renderedBashScript("/usr/bin/fish -lc 'echo hello'"))
}
@Test
fun `missing optional input is not displayed as null`() {
assertTrue(parseToolInput("TaskOutput", "null").rest.isEmpty())
}
@Test
fun `collaboration calls say what they do and omit empty completion`() {
assertEquals("Spawn agent", toolDisplayName("Task"))
assertEquals("Wait for agents", toolDisplayName("TaskOutput"))
assertEquals("", toolDisplayOutput("TaskOutput", "completed"))
assertEquals("failed", toolDisplayOutput("TaskOutput", "failed"))
}
}