Settings as a screen with two tabs, and fields that cost one line

The settings dialog had outgrown a dialog: it scrolled inside itself,
covered the session it is about, and had nowhere to put a second tab. It
is a screen now, drawn over the session like the file explorer so the
session under it stays composed, with the back gesture to leave it. The
second tab is ProviderScreen itself -- the same composable the machines
tab opens -- so a provider's settings have two ways in and one
implementation.

Every text field in the app goes through LabelledField: the label is a
line above the box rather than a thing floating inside it, the hint says
what leaving it blank means, and the padding is one line's worth.
Material's outlined field spends the height of three lines to hold one,
which on a form of a dozen settings is a screen and a half of scrolling.
The value's own text is unchanged -- the framing was what cost.

A session also gets a system prompt, which for llama.cpp is one entry in
the params table and no app change: it rides in front of the conversation
on every request rather than being recorded as the first thing in it, so
changing it takes effect on the next message. ParamKind::Prose is new
because a paragraph in a one-line box shows six words of itself.
This commit is contained in:
iris-ai committed 2026-09-21 03:35:54 -04:00
1 parent 386c1c4def
commit 7278a58387
17 files changed
+364 -176

No files matched your search

@@ -17,7 +17,6 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -57,7 +56,12 @@ fun ProviderScreen(
settings: ServerSettings,
machineId: String,
provider: String,
onBack: () -> Unit,
/**
* The way back, or null where this is drawn inside something that has one of its own -- the
* session settings screen's second tab. Two ways out stacked above each other is a reader
* asking which of them goes where.
*/
onBack: (() -> Unit)?,
) {
val scope = rememberCoroutineScope()
var state by remember { mutableStateOf<LoadState<ProviderView>>(LoadState.Loading) }
@@ -111,8 +115,13 @@ fun ProviderScreen(
// The models search at the bottom takes the keyboard, and everything below the field it is
// typed in -- the Search button, the results -- is behind it without this.
Column(Modifier.fillMaxSize().imePadding().padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
TextButton(onClick = onBack) { Text("Back") }
onBack?.let {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
TextButton(onClick = it) { Text("Back") }
}
}
when (val current = state) {
is LoadState.Loading -> CircularProgressIndicator()
@@ -331,14 +340,12 @@ private fun ServerCard(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
LabelledField(
label = "Models loaded at once",
value = typed,
onValueChange = { typed = it.filter(Char::isDigit) },
label = { Text("Models loaded at once") },
placeholder = { Text("one -- a second model replaces the first") },
singleLine = true,
hint = "one -- a second model replaces the first",
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
Row(verticalAlignment = Alignment.CenterVertically) {
// Shown whether or not it is running, and disabled when there is nothing to stop: