Answer a question card as one act, and mark the press that made it

Picking an option marked nothing until the answer had crossed the tunnel,
been recorded and come back as an event, so the card sat unchanged for most
of a second after a tap. What the reader has picked is now the card's own
state and shows at once; a Submit at the foot sends every question the tool
is waiting on, greyed until all of them have an answer and a spinner while
the request is out.

Several questions are paged rather than stacked, with the count and a pair
of arrows on the right, because three questions with four described options
each is several screens and the reader scrolls past the one they are
answering to reach the button that sends it.

A permission ask keeps its single tap -- two bare words are not worth a
submit step -- and marks what was pressed until the request settles, so the
mark either stands on the recorded answer or goes away with the failure.

Chevron draws all four directions from one description of the shape, since
the pager needed two more of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-03 20:04:48 -04:00
1 parent a4d512af26
commit 8dcd2cb708
7 files changed
+299 -127

No files matched your search

@@ -1094,6 +1094,19 @@ fun SessionScreen(
}
}
/**
* Sends every answer a question card handed over, and says when the last of them has settled.
*
* All of them in one go because a card asks its questions together and the tool is waiting on
* all of them; the completion is what turns the card's spinner back into a button, whether the
* server took them or refused.
*/
fun answerAll(answers: List<QuestionAnswer>, onSettled: () -> Unit) {
act(onDone = onSettled) {
answers.forEach { answerQuestion(settings, summary.id, it.questionId, it.answers) }
}
}
fun send() {
val text = input.text.trim()
val attachments = pendingAttachments
@@ -1509,16 +1522,7 @@ fun SessionScreen(
else expandedTools + id
}
},
onAnswer = { questionId, answers ->
act {
answerQuestion(
settings,
summary.id,
questionId,
answers,
)
}
},
onAnswer = ::answerAll,
image = { ref ->
SessionImage(
settings,
@@ -1568,16 +1572,7 @@ fun SessionScreen(
else expandedTools + item.id
}
},
onAnswer = { questionId, answers ->
act {
answerQuestion(
settings,
summary.id,
questionId,
answers,
)
}
},
onAnswer = ::answerAll,
image = { ref ->
SessionImage(
settings,
@@ -1588,16 +1583,7 @@ fun SessionScreen(
},
)
is TranscriptItem.QuestionCard ->
QuestionRow(item) { answers ->
act {
answerQuestion(
settings,
summary.id,
item.id,
answers,
)
}
}
QuestionRow(item, ::answerAll)
is TranscriptItem.ErrorMsg ->
Text(
item.message,
@@ -1691,7 +1677,7 @@ fun SessionScreen(
.semantics { contentDescription = "Jump to latest" },
) {
Chevron(
pointingUp = false,
Pointing.Down,
colour = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
)
@@ -2310,13 +2296,14 @@ private fun SessionStatusRow(
@Composable
private fun QuestionRow(
question: TranscriptItem.QuestionCard,
onAnswer: (List<String>) -> Unit,
onAnswer: (List<QuestionAnswer>, onSettled: () -> Unit) -> Unit,
) {
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp)) {
// The same body the questions on a tool call get: one question is the same
// thing whether or not something else asked it.
AskedQuestion(question, onAnswer)
// The same body the questions on a tool call get, down to the submit button: one
// question is the same thing whether or not something asked it, and two renderings of
// it would be two places for an answer to go missing.
AskUserQuestionBody(listOf(question), onAnswer)
}
}
}