Draw a background task's command as code, and frame the card it opens
A backgrounded command in a session's panel was a terminal glyph beside its words. The glyph said "command" and so did the monospace face, which is the same fact twice -- so the mark goes and the command is drawn the way every other verbatim thing in this app is: highlighted, monospace, on the raw surface. The glyph stays for the kinds whose description is prose, and for a command a provider never named. Tapping one landed the tool card against the bottom edge of the screen, since a reversed list anchors an item by its bottom -- so a tall card arrived at its last line. A card that fits the viewport is centred now, and one that does not has its top put at the top, which is where reading it starts. Only for a journey the reader asked for: a restore still puts them back exactly where they stopped. The panel's own close chevron is gone -- the drag, the scrim and Back all close it -- and the count is drawn even when it is zero, so "nothing is running" and "nobody has asked yet" stop looking identical. There is then nothing to expand, so that heading carries no chevron either. Checked on the emulator against the sandbox: the card centred in a mid-transcript tap, sat at the newest end where the list clamps, and the zero heading drew with no control.
This commit is contained in:
1 parent
cedb18e8c1
commit
df48a334f7
4 files changed
+114
-50
No files matched your search
@@ -1,5 +1,6 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -17,8 +18,10 @@ import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
@@ -34,9 +37,10 @@ import androidx.compose.ui.unit.dp
|
||||
* nobody asked after would push them off it. Expanding pushes them down instead of covering them,
|
||||
* so the two are read together.
|
||||
*
|
||||
* Nothing is drawn at all when the count is zero -- including when the provider never said, which
|
||||
* is the same absence the status row draws. A permanently visible "0 bg tasks" would be a line
|
||||
* about nothing on every session that has never backgrounded anything, which is most of them.
|
||||
* The count is drawn even when it is zero, in the same words. A section that appeared only once
|
||||
* something was running made its own presence the answer, and no heading at all draws "nothing is
|
||||
* running" and "nobody has asked yet" identically. There is then nothing to expand, so the heading
|
||||
* carries no chevron either: it is a statement rather than a control.
|
||||
*/
|
||||
fun LazyListScope.backgroundTaskSection(
|
||||
count: Int,
|
||||
@@ -46,21 +50,25 @@ fun LazyListScope.backgroundTaskSection(
|
||||
onRetry: () -> Unit,
|
||||
onOpenCall: (CallSite) -> Unit,
|
||||
) {
|
||||
if (count == 0) return
|
||||
item(key = "background-heading") {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp).clickable(onClick = onToggle),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.then(if (count == 0) Modifier else Modifier.clickable(onClick = onToggle)),
|
||||
) {
|
||||
Text(
|
||||
"${backgroundTaskLabel(count)} running",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Chevron(if (expanded) Pointing.Up else Pointing.Down)
|
||||
if (count > 0) Chevron(if (expanded) Pointing.Up else Pointing.Down)
|
||||
}
|
||||
}
|
||||
if (!expanded) return
|
||||
// The count as well as the switch: [tasks] is the last answer anybody got, so a section left
|
||||
// expanded as the work finished would draw cards for tasks that have ended.
|
||||
if (count == 0 || !expanded) return
|
||||
when (tasks) {
|
||||
is LoadState.Loading ->
|
||||
item(key = "background-loading") {
|
||||
@@ -102,7 +110,8 @@ fun LazyListScope.backgroundTaskSection(
|
||||
}
|
||||
|
||||
/**
|
||||
* One background task: what it is doing, drawn as one line with its kind as the mark beside it.
|
||||
* One background task: what it is doing, drawn as one line that says what kind it is by how it
|
||||
* looks.
|
||||
*
|
||||
* 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
|
||||
@@ -110,6 +119,10 @@ fun LazyListScope.backgroundTaskSection(
|
||||
* 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.
|
||||
*
|
||||
* A command needs no mark: drawn the way every other verbatim thing here is -- highlighted,
|
||||
* monospace, on [rawSurface] -- it says "this is a command" in the same appearance the tool card it
|
||||
* came from uses, and a mark beside that would be the same fact twice.
|
||||
*
|
||||
* [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
|
||||
@@ -119,6 +132,9 @@ fun LazyListScope.backgroundTaskSection(
|
||||
@Composable
|
||||
private fun BackgroundTaskCard(task: BackgroundTaskSummary, onOpen: (() -> Unit)?) {
|
||||
val look = backgroundTaskLook(task.kind)
|
||||
// Null where a provider named the task by a process id and nothing resolved a command out of
|
||||
// it: there is no code to draw, so the row takes the mark and the words instead.
|
||||
val command = task.description?.takeIf { look.code }
|
||||
OutlinedCard(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -134,31 +150,49 @@ private fun BackgroundTaskCard(task: BackgroundTaskSummary, onOpen: (() -> Unit)
|
||||
)
|
||||
.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 ?: 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 (command == null) {
|
||||
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.
|
||||
Text(
|
||||
task.description ?: look.words,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color =
|
||||
if (task.description == null) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else LocalContentColor.current,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else {
|
||||
// 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(
|
||||
// Not cached: one command line lexes in microseconds -- the cache exists for a
|
||||
// fence with two hundred lines in it.
|
||||
remember(command) { highlight(command, Language.SHELL) },
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier.weight(1f)
|
||||
// Smaller than the card's own radius, for the reason [RawBlock] rounds
|
||||
// its corners that way: this sits inside one.
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.background(rawSurface)
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp)
|
||||
// The fill says "command" to everybody else; this says it to a reader
|
||||
// who cannot see the fill.
|
||||
.semantics { contentDescription = "${look.words} $command" },
|
||||
)
|
||||
}
|
||||
if (onOpen != null) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Chevron(Pointing.Right)
|
||||
@@ -167,8 +201,14 @@ private fun BackgroundTaskCard(task: BackgroundTaskSummary, onOpen: (() -> Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/** How one kind of background task is drawn: see [backgroundTaskLook]. */
|
||||
private data class TaskLook(val glyph: String, val words: String, val mono: Boolean)
|
||||
/**
|
||||
* How one kind of background task is drawn: see [backgroundTaskLook].
|
||||
*
|
||||
* [code] is the kind whose description is verbatim text rather than prose, which is drawn as code
|
||||
* and takes no [glyph]; the glyph is still what a task of that kind falls back to when nothing said
|
||||
* what it ran.
|
||||
*/
|
||||
private data class TaskLook(val glyph: String, val words: String, val code: Boolean)
|
||||
|
||||
/**
|
||||
* Everything a [BackgroundTaskSummary.kind] decides, answered by one `when`.
|
||||
@@ -183,10 +223,10 @@ private data class TaskLook(val glyph: String, val words: String, val mono: Bool
|
||||
private fun backgroundTaskLook(kind: String) =
|
||||
when (kind) {
|
||||
// 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)
|
||||
"command" -> TaskLook(COMMAND_GLYPH, "background command", code = true)
|
||||
"agent" -> TaskLook(AGENT_GLYPH, "subagent", code = false)
|
||||
"workflow" -> TaskLook(WORKFLOW_GLYPH, "workflow", code = false)
|
||||
else -> TaskLook(UNKNOWN_GLYPH, "background task", code = false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user