Output a command produced can be selected and copied

A build's error is the tail of what the compiler actually said, and on a
phone there was no way to get it out: the machine that produced it is not
the machine in your hand, so a message you cannot copy is one you have to
retype into whatever you are fixing it with.

Theme.kt's OutputText is the one place that decides this, and every failure
message now goes through it -- a component's build or download, a service
action, a checkout's remote check, and the log dialog's own failure line.
The log body gets a SelectionContainer directly rather than going through
OutputText, since it is an AnnotatedString the ANSI renderer coloured inside
its own scrolling panel; Copy stays beside it for taking the whole thing.

The app's own words are deliberately left alone. Selection handles on a
status word like "failed" are noise, and a card that starts a selection on
long-press fights the gestures it already has -- so the split is by where
the words came from, not by how important they look.

Checked on the emulator against a failing build: long-pressing the error on
the card raises handles and the Copy toolbar, the state word above it does
not, and the log dialog still scrolls in both directions with selection
enabled.
This commit is contained in:
iris committed 2026-09-01 03:22:38 -04:00
1 parent 90082bd286
commit a7f7f4550e
4 files changed
+95 -48

No files matched your search

@@ -1704,8 +1704,9 @@ private fun AppCard(
// first time is both at once, and the failure is the more
// useful of the two.
when {
projectState is ProjectState.Error ->
Text(projectState.message, color = MaterialTheme.colorScheme.error)
// Git's own words, most of the time. Selectable, like every
// other message here that this app did not write.
projectState is ProjectState.Error -> OutputText(projectState.message)
!entry.built && !awaitingApproval ->
Text(
@@ -1730,11 +1731,7 @@ private fun AppCard(
// pressed a button is the one they are waiting to read.
if (projectState !is ProjectState.Error) {
entry.checkError?.let { reason ->
Text(
reason,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
OutputText(reason, style = MaterialTheme.typography.bodySmall)
}
}
}
@@ -2271,12 +2268,10 @@ private fun ComponentCard(
}
}
// The service script's own words about why it could not answer,
// so selectable for the same reason.
component.error?.let { reason ->
Text(
reason,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
OutputText(reason, style = MaterialTheme.typography.bodySmall)
}
// The caller's content and the service's own never appear
@@ -2365,12 +2360,12 @@ private fun ComponentCard(
// whether that was its build, its download, or a service
// action. In its own row rather than at the foot of the card,
// which could only ever have been the project's.
//
// Selectable, because this is the build's own output: the tail
// of what the compiler said, which is the thing somebody
// actually needs to copy somewhere.
(state as? ComponentState.Error)?.let {
Text(
it.message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
OutputText(it.message, style = MaterialTheme.typography.bodySmall)
}
// Only while the old app is actually still there. The build