Colour what a shell printed, swipe back, and let a stopped session take a setting
Bash output arrived with its escape sequences in it, so a coloured diff or test run was line noise around the thing being read. The sequences that decide how text looks are spans now and every other one is dropped, with a carriage return honoured the way a terminal honours it so a progress bar shows its final state rather than every state it passed through. A rightward drag anywhere on a session, spawn or settings screen steps back, following the finger so it can be abandoned. It loses every argument: a child that consumes horizontal drags -- a wide fence, a table, a selection -- has already taken the gesture before this sees it. Changing the model or the permission mode of a session with nothing running was refused, in words about the driver, while the config had already taken the value that its next start will use. Both now announce the stored setting instead, through one function, since which of the pair it is does not change the rule. The model-switch warning no longer fires after a clear: the server reports the context as unmeasured rather than zero afterwards, and the fallback reading counted the whole conversation still on screen. An image loading shows a spinner in the space it is about to fill, in the transcript and in the composer's attachments alike. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8dcd2cb708
commit
a383c19dd5
10 files changed
+651
-48
No files matched your search
@@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -20,6 +22,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
@@ -94,14 +97,19 @@ fun SessionImage(
|
||||
val heightPx = with(LocalDensity.current) { height.roundToPx() }
|
||||
Box(Modifier.fillMaxWidth().height(height), contentAlignment = Alignment.CenterStart) {
|
||||
when (val image = bitmap) {
|
||||
// Two states, not one: an image still arriving and an image that will never arrive
|
||||
// look nothing alike to a reader who can do something about the second. So one gets a
|
||||
// spinner in the space the picture is about to fill, and the other gets words.
|
||||
null ->
|
||||
Text(
|
||||
// Two states, not one: an image still arriving and an image that will never
|
||||
// arrive look nothing alike to a reader who can do something about the second.
|
||||
if (failed) "[image $ref unavailable]" else "[loading image…]",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
if (failed) {
|
||||
Text(
|
||||
"[image $ref unavailable]",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
LoadingImage(height)
|
||||
}
|
||||
else ->
|
||||
Image(
|
||||
bitmap = image,
|
||||
@@ -153,17 +161,51 @@ fun SessionImageViewer(
|
||||
// coming. Stated in white because this box paints its own black behind them and a
|
||||
// theme colour would be picked against a surface that is not there.
|
||||
null ->
|
||||
Text(
|
||||
if (failed) "Image $ref is unavailable" else "Loading image…",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
if (failed) {
|
||||
Text(
|
||||
"Image $ref is unavailable",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
} else {
|
||||
// The whole dialog is the area this picture is about to fill, so the
|
||||
// spinner sits in the middle of it. White for the same reason the words
|
||||
// beside it are: this box paints its own black, and a theme colour would
|
||||
// be chosen against a surface that is not there.
|
||||
CircularProgressIndicator(color = Color.White)
|
||||
}
|
||||
else -> ZoomableImage(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The room a picture is about to take, with a spinner in the middle of it.
|
||||
*
|
||||
* A square of the row's own height rather than the full width of the transcript: the height is what
|
||||
* [SessionImage] reserves and the width is not known until the bytes arrive, so a full-width
|
||||
* placeholder would promise a picture wider than most of them turn out to be. Square is the closest
|
||||
* thing to "the size of it" that can be drawn before knowing.
|
||||
*
|
||||
* Tinted, so the reader can see that something is being kept for a picture. That is also what
|
||||
* distinguishes it from the failure beside it, which is words on the ordinary surface.
|
||||
*/
|
||||
@Composable
|
||||
private fun LoadingImage(height: Dp) {
|
||||
Box(
|
||||
Modifier.size(height)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(Modifier.size(LOADING_SPINNER), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
|
||||
/** Small enough to sit inside the thumbnail's square without filling it. */
|
||||
private val LOADING_SPINNER = 24.dp
|
||||
|
||||
/**
|
||||
* Four lines of the body style the transcript is set in.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user