Draw a background task as what it ran, and go there on a tap
A card in the session's panel said "background command" under every description -- and for Codex, which names a terminal by a process id and gives no description at all, that phrase was the whole of every card. Both halves of the answer are in the transcript rather than in what the provider says: a driver now reports which tool call its task belongs to (Claude's `task_started` carries the `tool_use_id`, Codex's terminal list the `itemId`), and `LiveSession::background_tasks` resolves those ids against the transcript into a sequence number and, where the provider said nothing, the command the call was made with. So the card draws the command, and the kind shrinks to a mark beside it whose name is what a screen reader is given. Tapping one goes to that call in the transcript, opened, which is where a backgrounded command's output already lands -- rather than drawing a second copy of it beside the panel. The journey is the one a reopened session already makes to put a reader back where they stopped, now one function (`travelTo`). It has to release the held backlog first: events arriving while the reader is away from the newest end are held rather than applied, so a task started since they scrolled back was in no row at all and the tap looked like it had done nothing. Verified against the sandbox on the emulator: the panel draws `sleep 120 && echo done` for an echo session's `/background`, and tapping it lands on that Bash card with its output showing.
This commit is contained in:
1 parent
3b309766d7
commit
cedb18e8c1
18 files changed
+635
-151
No files matched your search
@@ -19,6 +19,10 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
@@ -40,6 +44,7 @@ fun LazyListScope.backgroundTaskSection(
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onOpenCall: (CallSite) -> Unit,
|
||||
) {
|
||||
if (count == 0) return
|
||||
item(key = "background-heading") {
|
||||
@@ -86,58 +91,102 @@ fun LazyListScope.backgroundTaskSection(
|
||||
)
|
||||
}
|
||||
else ->
|
||||
uniqueItems(rows, key = { "background-${it.id}" }) { BackgroundTaskCard(it) }
|
||||
uniqueItems(rows, key = { "background-${it.id}" }) { task ->
|
||||
BackgroundTaskCard(
|
||||
task,
|
||||
onOpen = task.call?.let { call -> { onOpenCall(call) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One background task: what it is doing, and what kind of thing is doing it.
|
||||
* One background task: what it is doing, drawn as one line with its kind as the mark beside it.
|
||||
*
|
||||
* Not something to open, unlike the subagent cards below it -- a task is a provider's runtime state
|
||||
* and has no transcript of its own. A backgrounded agent that does is also in the subagent list,
|
||||
* under its own name.
|
||||
* The kind used to be a second line under the words, which on a list of backgrounded commands was
|
||||
* "background command" repeated down the panel -- and for a provider that names a task by a process
|
||||
* id it was the *whole* card, so every row said the same two words. A mark carries the same
|
||||
* difference in a width the text does not have to make room for, and it is the [Glyph]'s
|
||||
* description that keeps the words for anybody who cannot see it.
|
||||
*
|
||||
* [onOpen] is where the call that started this is in the transcript, for the readers who tap it:
|
||||
* null where the provider never said which call it was, or where that call is no longer in the
|
||||
* transcript, and the card is then a statement rather than a control. The chevron is what says
|
||||
* which of the two this is, since a card that quietly does nothing when pressed is worse than one
|
||||
* that never invited the press.
|
||||
*/
|
||||
@Composable
|
||||
private fun BackgroundTaskCard(task: BackgroundTaskSummary) {
|
||||
val kind = backgroundTaskKindLabel(task.kind)
|
||||
private fun BackgroundTaskCard(task: BackgroundTaskSummary, onOpen: (() -> Unit)?) {
|
||||
val look = backgroundTaskLook(task.kind)
|
||||
OutlinedCard(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||
// The kind stands in as the title where the provider gave no description, rather than
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.then(
|
||||
if (onOpen == null) Modifier
|
||||
else
|
||||
Modifier.clickable(
|
||||
onClickLabel = "Show where this started",
|
||||
onClick = onOpen,
|
||||
)
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
) {
|
||||
Glyph(
|
||||
look.glyph,
|
||||
colour = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.semantics { contentDescription = look.words },
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
// The kind stands in as the words where the provider gave no description, rather than
|
||||
// the id it named the task by: Codex reports a process number, which says nothing to
|
||||
// the person reading and would look like a name somebody chose.
|
||||
//
|
||||
// Cut at its tail: what identifies a command is the program at its head, and the long
|
||||
// ones are exactly the ones being read closely.
|
||||
Text(
|
||||
task.description ?: kind,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
task.description ?: look.words,
|
||||
style =
|
||||
if (look.mono)
|
||||
MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace)
|
||||
else MaterialTheme.typography.bodyMedium,
|
||||
color =
|
||||
if (task.description == null) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else LocalContentColor.current,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (task.description != null) {
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
kind,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
if (onOpen != null) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Chevron(Pointing.Right)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** How one kind of background task is drawn: see [backgroundTaskLook]. */
|
||||
private data class TaskLook(val glyph: String, val words: String, val mono: Boolean)
|
||||
|
||||
/**
|
||||
* What a [BackgroundTaskSummary.kind] is called on screen.
|
||||
* Everything a [BackgroundTaskSummary.kind] decides, answered by one `when`.
|
||||
*
|
||||
* A kind this build has not heard of is named by what every one of them has in common rather than
|
||||
* by the nearest word we do know, which would be this screen asserting something the server never
|
||||
* said.
|
||||
* One rather than three, which is the rule this screen already learned once with the status word
|
||||
* and its colour: three `when`s over one set is two of them waiting to miss a member.
|
||||
*
|
||||
* A kind this build has not heard of takes the question mark and is named by what every one of them
|
||||
* has in common. The nearest word or mark we do know -- a robot, a terminal -- would be this screen
|
||||
* deciding what the server meant by a word it invented after this build shipped.
|
||||
*/
|
||||
private fun backgroundTaskKindLabel(kind: String) =
|
||||
private fun backgroundTaskLook(kind: String) =
|
||||
when (kind) {
|
||||
"agent" -> "subagent"
|
||||
"command" -> "background command"
|
||||
"workflow" -> "workflow"
|
||||
else -> "background task"
|
||||
// A command is drawn in the face a command is drawn in everywhere else here.
|
||||
"command" -> TaskLook(COMMAND_GLYPH, "background command", mono = true)
|
||||
"agent" -> TaskLook(AGENT_GLYPH, "subagent", mono = false)
|
||||
"workflow" -> TaskLook(WORKFLOW_GLYPH, "workflow", mono = false)
|
||||
else -> TaskLook(UNKNOWN_GLYPH, "background task", mono = false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user