Merge remote-tracking branch 'origin/main'

This commit is contained in:
iris committed 2026-08-29 22:29:18 -04:00
commit bb191eec21
17 files changed
+627 -310

No files matched your search

@@ -27,8 +27,8 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
@@ -316,7 +316,6 @@ fun SessionScreen(
settings: ServerSettings,
summary: SessionSummary,
onBack: () -> Unit,
onUsage: () -> Unit,
onSettings: () -> Unit,
) {
val scope = rememberCoroutineScope()
@@ -737,12 +736,18 @@ fun SessionScreen(
}
}
// One poll for this machine's limits, read by the two things that show them: the bar under
// the header, and the colour of the button that opens the dialog.
val usage = rememberSessionUsage(settings, summary.setup)
var usageOpen by remember { mutableStateOf(false) }
Column(Modifier.fillMaxSize()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
) {
TextButton(onClick = onBack) { Text("Back") }
GlyphButton(BACK_GLYPH, "Back", onBack)
Spacer(Modifier.width(8.dp))
Column(Modifier.weight(1f)) {
Text(title, style = MaterialTheme.typography.titleMedium)
Text(
@@ -766,22 +771,29 @@ fun SessionScreen(
// the paid service's own numbers, so a session on a provider with no such service
// gets an honest "unavailable" rather than a hidden button -- a control that comes
// and goes makes its absence the signal, and absence cannot say why.
TextButton(onClick = onUsage) { Text("Usage") }
// A step down from this session, so it sits at the end of the session's own row.
// The name is the whole of what it holds today, which is why it is a gear and not a
// word: there will be more, and a bar of words has nowhere to put it.
IconButton(
onClick = onSettings,
modifier = Modifier.semantics { contentDescription = "Session settings" },
) {
Gear()
// Coloured by the worst window behind it, so the row says whether the limits are
// worth opening before anybody opens them. Blue at every ordinary level and only
// yellow or red near a limit -- and the theme's plain control colour whenever there
// is no measurement, since blue is the low end of the scale here and would read as
// "checked, and fine" about a machine nobody could reach.
Row(horizontalArrangement = Arrangement.spacedBy(GLYPH_BUTTON_GAP)) {
GlyphButton(
USAGE_GLYPH,
"Usage",
{ usageOpen = true },
colour = usageGlyphColour(usage),
)
// A step down from this session, so it sits at the end of the session's own row.
// The name is the whole of what it holds today, which is why it is a cog and not
// a word: there will be more, and a bar of words has nowhere to put it.
GlyphButton(SETTINGS_GLYPH, "Session settings", onSettings)
}
}
// Under the header, above everything the session itself says: it is a fact about the
// machine rather than a turn in the conversation, and it is the number that decides
// whether to keep going -- which was a screen away from where that gets decided.
SessionUsageBar(settings = settings, setup = summary.setup)
SessionUsageBar(usage)
(streamError ?: actionError)?.let { message ->
Text(
@@ -1052,21 +1064,46 @@ fun SessionScreen(
}
if (running) {
OutlinedButton(onClick = { act { interruptSession(settings, summary.id) } }) {
Text("Stop")
// A filled square, which is what stop has looked like since tape decks.
// Outlined beside the filled Send, so the pair still reads as one primary
// action and one secondary -- the glyphs changed, the weighting did not.
Glyph(
STOP_GLYPH,
colour = LocalContentColor.current,
modifier = Modifier.semantics { contentDescription = "Stop" },
)
}
Spacer(Modifier.width(8.dp))
}
// "Queue" while a turn is in flight, because that is what
// sending then does: the message is injected at the next
// tool boundary rather than starting a turn of its own.
// Naming it Send there would promise something immediate
// and describe something that waits.
Button(onClick = { send() }) { Text(if (running) "Queue" else "Send") }
// The paper plane alone when it means send. While a turn is in flight it keeps
// the word "Queue" beside it, because that is what sending then does -- the
// message is injected at the next tool boundary rather than starting a turn of
// its own -- and an icon that does two things while looking identical would
// promise something immediate and do something that waits. The word is also the
// button's accessible name, which is all a screen reader gets either way.
Button(onClick = { send() }) {
Glyph(
SEND_GLYPH,
colour = LocalContentColor.current,
modifier = Modifier.semantics { contentDescription = sendLabel(running) },
)
if (running) {
Spacer(Modifier.width(8.dp))
Text(sendLabel(running))
}
}
}
}
}
if (usageOpen) {
UsageDialog(settings = settings, onDismiss = { usageOpen = false })
}
}
/** What pressing Send does right now, said the same way to the eye and to a screen reader. */
private fun sendLabel(running: Boolean) = if (running) "Queue" else "Send"
/**
* An inline transcript image, fetched (authenticated, pinned) from the session's files route. The
* bitmap is remembered per ref, so scrolling doesn't refetch.