One control per kind of setting, and no paragraphs under any of them
Three explanatory paragraphs were left on the session settings screen: what auto-resume does, what Move costs, what changing the thinking level costs. The last two are consequences of an action, so they are `RestartDialog` now -- the same question the machines tab already asks before it reloads a model, and now in one file rather than private to that screen. The first is gone; the switch beside it says what it is. Thinking was two controls: a row of chips for a llama session, where it is a provider-declared choice, and a picker button for a Claude session, where it is the session's own effort. One kind of information drawn two ways, decided by which code path the value came down. A choice is `PickerRow` in a list of settings and `ChipGroup` on a form being filled in, and which of the two is the screen's to say rather than the provider's. `warnAboutRestart` went with them. It marked a control "(on restart)" and wrote a sentence under the form, and it had nothing to mark: every session param is `restart: false` and every model param is `restart: true`. The field now decides whether saving a model's settings stops to ask, which is the question it was always about. The wait's bar keeps the status row's own margin instead of running to the edges of the glass. Checked on the emulator against the sandbox: an echo, a claude-cli and a llama session, each with no paragraph left under a setting and thinking drawn the same way in all three; "Think high?", "Move to /tmp?" and the model settings dialog all stop to ask. ktfmt, compile, lint and the unit tests are clean.
This commit is contained in:
1 parent
5626a7d595
commit
3dbf04f5ec
7 files changed
+178
-132
No files matched your search
@@ -185,8 +185,19 @@ Module-by-module intent is in PLAN.md's "Backend layout".
|
|||||||
line's worth. Material's outlined field spends the height of three lines to
|
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
|
hold one, which on a form of a dozen settings is a screen and a half of
|
||||||
scrolling. What is *not* shrunk is the value -- the framing is what was
|
scrolling. What is *not* shrunk is the value -- the framing is what was
|
||||||
expensive. A field's `hint` is what leaving it blank means, above the box
|
expensive. A field's `hint` is what leaving it blank means, drawn inside the
|
||||||
with the label, because inside it is gone the moment anybody types.
|
empty box: **a setting is a title and a control and nothing else**, so no
|
||||||
|
explanatory line sits between them and no paragraph sits under them.
|
||||||
|
**What a setting costs is asked rather than written down** -- `RestartDialog`
|
||||||
|
is that question wherever it comes up (a model's settings, the shared
|
||||||
|
server's, moving a session's directory, changing its thinking level), because
|
||||||
|
each of them ends something that is running, and a sentence beside the control
|
||||||
|
is read after the decision if at all.
|
||||||
|
**One kind of information gets one control.** A choice is `PickerRow` in a
|
||||||
|
list of settings and `ChipGroup` on a form being filled in, and which of the
|
||||||
|
two is the screen's to say (`ProviderParamFields`' `choices`) rather than the
|
||||||
|
provider's -- a llama session's thinking level and a Claude session's have to
|
||||||
|
look the same.
|
||||||
**Session settings is a screen with two tabs** (`SessionSettingsScreen.kt`),
|
**Session settings is a screen with two tabs** (`SessionSettingsScreen.kt`),
|
||||||
drawn over the session like the file explorer so the session stays composed.
|
drawn over the session like the file explorer so the session stays composed.
|
||||||
The second tab is `ProviderScreen` itself -- the same composable the machines
|
The second tab is `ProviderScreen` itself -- the same composable the machines
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -31,11 +30,11 @@ fun ProviderParamFields(
|
|||||||
values: Map<String, String>,
|
values: Map<String, String>,
|
||||||
onChange: (Map<String, String>) -> Unit,
|
onChange: (Map<String, String>) -> Unit,
|
||||||
/**
|
/**
|
||||||
* Whether to say which settings wait for a restart. False on a spawn form, where nothing is
|
* How a choice is drawn here, which is the screen's to decide rather than the setting's: chips
|
||||||
* running yet and every setting is about to be read — saying it there would be a warning about
|
* on a form somebody is filling in, a picker row in a list of settings. A provider's choices
|
||||||
* a state the reader cannot be in.
|
* have to look like the choices beside them, whichever screen that is.
|
||||||
*/
|
*/
|
||||||
warnAboutRestart: Boolean,
|
choices: ChoiceStyle,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (specs.isEmpty()) return
|
if (specs.isEmpty()) return
|
||||||
@@ -55,16 +54,22 @@ fun ProviderParamFields(
|
|||||||
// `ParamKind::Choice`. Without that the picker could show a default it could
|
// `ParamKind::Choice`. Without that the picker could show a default it could
|
||||||
// not return to.
|
// not return to.
|
||||||
val default = spec.options.firstOrNull().orEmpty()
|
val default = spec.options.firstOrNull().orEmpty()
|
||||||
ChipGroup(
|
val selected = values[spec.key] ?: default
|
||||||
label = spec.label + restartSuffix(spec, warnAboutRestart),
|
val pick = { chosen: String -> set(if (chosen == default) "" else chosen) }
|
||||||
options = spec.options,
|
when (choices) {
|
||||||
selected = values[spec.key] ?: default,
|
ChoiceStyle.Chips ->
|
||||||
onSelect = { chosen -> set(if (chosen == default) "" else chosen) },
|
ChipGroup(
|
||||||
)
|
label = spec.label,
|
||||||
|
options = spec.options,
|
||||||
|
selected = selected,
|
||||||
|
onSelect = pick,
|
||||||
|
)
|
||||||
|
ChoiceStyle.Picker -> PickerRow(spec.label, selected, spec.options, pick)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else ->
|
else ->
|
||||||
LabelledField(
|
LabelledField(
|
||||||
label = spec.label + restartSuffix(spec, warnAboutRestart),
|
label = spec.label,
|
||||||
value = values[spec.key].orEmpty(),
|
value = values[spec.key].orEmpty(),
|
||||||
onValueChange = set,
|
onValueChange = set,
|
||||||
hint = spec.unset,
|
hint = spec.unset,
|
||||||
@@ -76,25 +81,14 @@ fun ProviderParamFields(
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
if (warnAboutRestart && specs.any { it.restart }) {
|
|
||||||
Text(
|
|
||||||
"A setting marked “on restart” is saved now and read when this session's process " +
|
|
||||||
"next starts.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Which control a [ParamKind.Choice] gets -- see [ProviderParamFields]'s `choices`. */
|
||||||
* Marks a control whose value will not take effect yet.
|
enum class ChoiceStyle {
|
||||||
*
|
Chips,
|
||||||
* On the label rather than beside it, because the reader decides whether to change the thing before
|
Picker,
|
||||||
* they touch it — a note underneath is read after the decision.
|
}
|
||||||
*/
|
|
||||||
private fun restartSuffix(spec: ParamSpec, warn: Boolean): String =
|
|
||||||
if (warn && spec.restart) " (on restart)" else ""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The keyboard for a value's shape. A number field that opens the letter keyboard is one every
|
* The keyboard for a value's shape. A number field that opens the letter keyboard is one every
|
||||||
|
|||||||
@@ -376,30 +376,6 @@ private fun ServerCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* What a setting read at load time costs, asked before it is written.
|
|
||||||
*
|
|
||||||
* One shape for both of the questions on this screen — a model's settings and the server's —
|
|
||||||
* because they are the same question: this is read when something starts again, and here is what
|
|
||||||
* starting again costs. A paragraph under the control said the same thing and was read after the
|
|
||||||
* decision, if at all.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun RestartDialog(
|
|
||||||
title: String,
|
|
||||||
text: String,
|
|
||||||
onConfirm: () -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(title) },
|
|
||||||
text = { Text(text) },
|
|
||||||
confirmButton = { TextButton(onClick = onConfirm) { Text("Save") } },
|
|
||||||
dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ModelCard(
|
private fun ModelCard(
|
||||||
model: ProviderModel,
|
model: ProviderModel,
|
||||||
@@ -485,7 +461,10 @@ private fun ModelSettingsDialog(
|
|||||||
// Asked over this dialog rather than instead of it, so Cancel comes back to the edits rather
|
// Asked over this dialog rather than instead of it, so Cancel comes back to the edits rather
|
||||||
// than throwing them away.
|
// than throwing them away.
|
||||||
var confirming by remember(model.id) { mutableStateOf(false) }
|
var confirming by remember(model.id) { mutableStateOf(false) }
|
||||||
val loaded = model.status == "loaded" || model.status == "sleeping"
|
// Whether saving costs anything worth asking about: something has to be in memory, and at
|
||||||
|
// least one of these settings has to be one it read on the way in.
|
||||||
|
val reloads =
|
||||||
|
(model.status == "loaded" || model.status == "sleeping") && specs.any { it.restart }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
// Every control here is a number, so the keyboard is up for most of this dialog's life --
|
// Every control here is a number, so the keyboard is up for most of this dialog's life --
|
||||||
@@ -501,14 +480,13 @@ private fun ModelSettingsDialog(
|
|||||||
specs = specs,
|
specs = specs,
|
||||||
values = params,
|
values = params,
|
||||||
onChange = { params = it },
|
onChange = { params = it },
|
||||||
// Every one of these is read at load time, which is what Save stops to say --
|
// Chips: this is a form of its own rather than a row in a list of settings.
|
||||||
// marking each control "on restart" would repeat it six times over.
|
choices = ChoiceStyle.Chips,
|
||||||
warnAboutRestart = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { if (loaded) confirming = true else onSave(params) }) {
|
TextButton(onClick = { if (reloads) confirming = true else onSave(params) }) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.example.aiapp
|
||||||
|
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a setting costs, asked before it is written.
|
||||||
|
*
|
||||||
|
* Every setting in this app whose consequence is worth saying says it here rather than in a
|
||||||
|
* paragraph beside the control: a sentence under a switch is read after the decision if it is read
|
||||||
|
* at all, and a form of a dozen settings each carrying its own explanation is mostly explanation. A
|
||||||
|
* modal interrupts at the moment the consequence becomes real, and it is also a way out.
|
||||||
|
*
|
||||||
|
* What they have in common, and why it is one dialog rather than four: each of them ends something
|
||||||
|
* that is running — a process, a loaded model, a server — and says what starting it again costs.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RestartDialog(
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
/** The word on the button, which is the action rather than a bare "OK". */
|
||||||
|
confirm: String = "Save",
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = { Text(text) },
|
||||||
|
confirmButton = { TextButton(onClick = onConfirm) { Text(confirm) } },
|
||||||
|
dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2813,12 +2813,17 @@ private fun SessionStatusRow(
|
|||||||
) {
|
) {
|
||||||
DebugStats.count("status row recomposed")
|
DebugStats.count("status row recomposed")
|
||||||
Column(modifier.fillMaxWidth()) {
|
Column(modifier.fillMaxWidth()) {
|
||||||
// The wait's bar, the whole width and above the words. In the width left over beside
|
// The wait's bar, above the words and across the width they have -- the row's own margin,
|
||||||
// them it stood where the context figure goes, which is what a reader wants to keep
|
// so it lines up with the rest of the screen rather than running to the edges of the
|
||||||
// seeing while a turn is read. The height is held whether or not there is a bar: this
|
// glass. In the width left over beside the words it stood where the context figure goes,
|
||||||
// sits above the transcript and the box, and one that came and went would move both
|
// which is what a reader wants to keep seeing while a turn is read. The height is held
|
||||||
// under the reader's thumb every time a turn started.
|
// whether or not there is a bar: one that came and went would move the transcript and the
|
||||||
Box(Modifier.fillMaxWidth().height(PROGRESS_BAR_HEIGHT)) {
|
// box under the reader's thumb every time a turn started.
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.padding(horizontal = STATUS_ROW_MARGIN)
|
||||||
|
.height(PROGRESS_BAR_HEIGHT)
|
||||||
|
) {
|
||||||
when {
|
when {
|
||||||
// Indeterminate for a compaction, which is a statement rather than an omission:
|
// Indeterminate for a compaction, which is a statement rather than an omission:
|
||||||
// the CLI says one has begun and then nothing at all until it has finished --
|
// the CLI says one has begun and then nothing at all until it has finished --
|
||||||
@@ -2855,6 +2860,9 @@ private fun SessionStatusRow(
|
|||||||
/** How tall a wait's bar is, and the space kept for one when there is no wait. */
|
/** How tall a wait's bar is, and the space kept for one when there is no wait. */
|
||||||
private val PROGRESS_BAR_HEIGHT = 4.dp
|
private val PROGRESS_BAR_HEIGHT = 4.dp
|
||||||
|
|
||||||
|
/** What the status row keeps clear at each side, which its bar lines up with. */
|
||||||
|
private val STATUS_ROW_MARGIN = 12.dp
|
||||||
|
|
||||||
/** The words of [SessionStatusRow]: what the session is doing, and what it is holding. */
|
/** The words of [SessionStatusRow]: what the session is doing, and what it is holding. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatusWords(
|
private fun StatusWords(
|
||||||
@@ -2868,7 +2876,7 @@ private fun StatusWords(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = STATUS_ROW_MARGIN, vertical = 4.dp),
|
||||||
) {
|
) {
|
||||||
when (status) {
|
when (status) {
|
||||||
// No spinner: a compaction's bar is above, and nothing arrives in the transcript
|
// No spinner: a compaction's bar is above, and nothing arrives in the transcript
|
||||||
@@ -3054,6 +3062,23 @@ private const val ONE_TAP_MS = 250L
|
|||||||
* The button *is* the current setting rather than a label beside one, so the row says what the
|
* The button *is* the current setting rather than a label beside one, so the row says what the
|
||||||
* session is set to without spending a second line on saying it.
|
* session is set to without spending a second line on saying it.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* One setting that is a choice from a list: what it is on the left, what it is set to on the right.
|
||||||
|
*
|
||||||
|
* The one shape for a choice in a list of settings, wherever the choice comes from -- a session's
|
||||||
|
* model, its permission mode, how hard it thinks, and every `ParamKind::Choice` a provider
|
||||||
|
* declares. They were two controls until 2026-09-21, so how hard a llama session thought was a row
|
||||||
|
* of chips and how hard a Claude session thought was this, which is two appearances for one kind of
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PickerRow(label: String, current: String, options: List<String>, onPick: (String) -> Unit) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(label, modifier = Modifier.weight(1f))
|
||||||
|
PickerButton(current = current, options = options, onPick = onPick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PickerButton(
|
fun PickerButton(
|
||||||
current: String,
|
current: String,
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ fun SessionSettingsScreen(
|
|||||||
var typedCwd by remember(sessionId) { mutableStateOf("") }
|
var typedCwd by remember(sessionId) { mutableStateOf("") }
|
||||||
var cwdError by remember { mutableStateOf<String?>(null) }
|
var cwdError by remember { mutableStateOf<String?>(null) }
|
||||||
var movingCwd by remember { mutableStateOf(false) }
|
var movingCwd by remember { mutableStateOf(false) }
|
||||||
|
// The two settings on this screen that end the session's process, held while the reader is
|
||||||
|
// asked whether that is what they meant. Null is nobody being asked.
|
||||||
|
var askedCwd by remember(sessionId) { mutableStateOf<String?>(null) }
|
||||||
|
var askedEffort by remember(sessionId) { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(sessionId) {
|
LaunchedEffect(sessionId) {
|
||||||
try {
|
try {
|
||||||
@@ -186,9 +190,15 @@ fun SessionSettingsScreen(
|
|||||||
/**
|
/**
|
||||||
* Moves the session, which ends the process that is in the old directory.
|
* Moves the session, which ends the process that is in the old directory.
|
||||||
*
|
*
|
||||||
* Said plainly beside the field rather than confirmed in a second dialog: what it costs is a
|
* Only ever reached through [RestartDialog], which is where what it costs is said -- see
|
||||||
* process, and a stopped session is a state this app already has a word and a button for.
|
* `askToMove`.
|
||||||
*/
|
*/
|
||||||
|
fun askToMove() {
|
||||||
|
val chosen = typedCwd.trim()
|
||||||
|
if (movingCwd || chosen.isEmpty() || chosen == cwd) return
|
||||||
|
askedCwd = chosen
|
||||||
|
}
|
||||||
|
|
||||||
fun moveCwd() {
|
fun moveCwd() {
|
||||||
val chosen = typedCwd.trim()
|
val chosen = typedCwd.trim()
|
||||||
if (movingCwd || chosen.isEmpty() || chosen == cwd) return
|
if (movingCwd || chosen.isEmpty() || chosen == cwd) return
|
||||||
@@ -209,7 +219,8 @@ fun SessionSettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chooses a thinking level, which ends the process the old level was launched with.
|
* Chooses a thinking level, which ends the process the old level was launched with. Asked for
|
||||||
|
* first, the same way a move is.
|
||||||
*
|
*
|
||||||
* Put back if the request is refused, for the reason the notification switch below gives: a
|
* Put back if the request is refused, for the reason the notification switch below gives: a
|
||||||
* control that stays where it was put after a refusal is stating something untrue.
|
* control that stays where it was put after a refusal is stating something untrue.
|
||||||
@@ -416,24 +427,14 @@ fun SessionSettingsScreen(
|
|||||||
label = "Message to send",
|
label = "Message to send",
|
||||||
value = resumeMessage,
|
value = resumeMessage,
|
||||||
onValueChange = { resumeMessage = it },
|
onValueChange = { resumeMessage = it },
|
||||||
// What an empty field means, said above it: the server's own word rather than
|
// What an empty field means: the server's own word rather than a session
|
||||||
// a session poked with nothing to read.
|
// poked with nothing to read.
|
||||||
hint = DEFAULT_RESUME_MESSAGE,
|
hint = DEFAULT_RESUME_MESSAGE,
|
||||||
enabled = autoResume == true,
|
enabled = autoResume == true,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
keyboardActions =
|
keyboardActions =
|
||||||
KeyboardActions(onDone = { setAutoResume(true, resumeMessage) }),
|
KeyboardActions(onDone = { setAutoResume(true, resumeMessage) }),
|
||||||
)
|
)
|
||||||
// What it does and what it costs, in the order it happens. The last sentence is the
|
|
||||||
// one that matters: the time below is when the server will *ask*, not a promise
|
|
||||||
// about when the session speaks.
|
|
||||||
Text(
|
|
||||||
"When this session stops because the account is out of quota, the server " +
|
|
||||||
"checks the limit and sends this message once it has lifted. It checks " +
|
|
||||||
"again if the limit is still on.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
// Only where something is actually waiting. Absent is not a state worth a row: a
|
// Only where something is actually waiting. Absent is not a state worth a row: a
|
||||||
// session that has not hit a limit has nothing scheduled, which the reader can see
|
// session that has not hit a limit has nothing scheduled, which the reader can see
|
||||||
// from the switch.
|
// from the switch.
|
||||||
@@ -463,17 +464,16 @@ fun SessionSettingsScreen(
|
|||||||
label = "Working directory",
|
label = "Working directory",
|
||||||
value = typedCwd,
|
value = typedCwd,
|
||||||
onValueChange = { typedCwd = it },
|
onValueChange = { typedCwd = it },
|
||||||
// What the field cannot say by being empty: a session that was never given
|
// What the field cannot say by being empty: a session that was never
|
||||||
// one starts wherever its launcher does, and this names that rather than
|
// given one starts wherever its launcher does.
|
||||||
// showing a path nobody chose.
|
|
||||||
hint = "wherever the session was started",
|
hint = "wherever the session was started",
|
||||||
enabled = cwd != null && !movingCwd,
|
enabled = cwd != null && !movingCwd,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
keyboardActions = KeyboardActions(onDone = { moveCwd() }),
|
keyboardActions = KeyboardActions(onDone = { askToMove() }),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { moveCwd() },
|
onClick = { askToMove() },
|
||||||
enabled =
|
enabled =
|
||||||
cwd != null &&
|
cwd != null &&
|
||||||
!movingCwd &&
|
!movingCwd &&
|
||||||
@@ -483,15 +483,6 @@ fun SessionSettingsScreen(
|
|||||||
Text(if (movingCwd) "Moving..." else "Move")
|
Text(if (movingCwd) "Moving..." else "Move")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The whole of what pressing Move does, where it is about to be pressed. A
|
|
||||||
// directory is settled when the process is spawned, so it is ended and the next
|
|
||||||
// thing said to the session starts it in the new place.
|
|
||||||
Text(
|
|
||||||
"Moving stops the session's process. It starts again in the new directory " +
|
|
||||||
"with the next message, or with Start.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
cwdError?.let {
|
cwdError?.let {
|
||||||
Text(
|
Text(
|
||||||
it,
|
it,
|
||||||
@@ -501,48 +492,23 @@ fun SessionSettingsScreen(
|
|||||||
}
|
}
|
||||||
choices.forEach { choice ->
|
choices.forEach { choice ->
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(
|
PickerRow(choice.label, choice.current, choice.options, choice.onPick)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Text(choice.label, modifier = Modifier.weight(1f))
|
|
||||||
PickerButton(
|
|
||||||
current = choice.current,
|
|
||||||
options = choice.options,
|
|
||||||
onPick = choice.onPick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Left out rather than disabled, the one place this dialog does that: a disabled
|
// Left out rather than disabled, the one place this dialog does that: a disabled
|
||||||
// control teaches what the thing can do, and a llama session cannot do this at all
|
// control teaches what the thing can do, and a llama session cannot do this at all
|
||||||
// -- the row would be teaching something false about it.
|
// -- the row would be teaching something false about it.
|
||||||
if (takesEffort) {
|
if (takesEffort) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Row(
|
PickerRow(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
"Thinking",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
effort ?: DEFAULT_EFFORT,
|
||||||
) {
|
// The level the CLI picks for itself is in the list as well as in the
|
||||||
Text("Thinking", modifier = Modifier.weight(1f))
|
// button, so leaving a level is not a one-way trip -- the same correction
|
||||||
PickerButton(
|
// the model picker carries.
|
||||||
current = effort ?: DEFAULT_EFFORT,
|
listOf(DEFAULT_EFFORT) + EFFORT_LEVELS,
|
||||||
// The level the CLI picks for itself is in the list as well as in the
|
) { chosen ->
|
||||||
// button, so leaving a level is not a one-way trip -- the same
|
askedEffort = chosen
|
||||||
// correction the model picker carries.
|
|
||||||
options = listOf(DEFAULT_EFFORT) + EFFORT_LEVELS,
|
|
||||||
onPick = { chosen ->
|
|
||||||
setEffort(chosen.takeIf { it != DEFAULT_EFFORT })
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// What it costs, said where it is about to be pressed, like Move above: the
|
|
||||||
// CLI reads the level when it launches and has no control request for
|
|
||||||
// changing one.
|
|
||||||
Text(
|
|
||||||
"Changing this stops the session's process. It starts again with the " +
|
|
||||||
"next message, or with Start.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
effortError?.let {
|
effortError?.let {
|
||||||
Text(
|
Text(
|
||||||
it,
|
it,
|
||||||
@@ -565,7 +531,9 @@ fun SessionSettingsScreen(
|
|||||||
specs = paramSpecs,
|
specs = paramSpecs,
|
||||||
values = params,
|
values = params,
|
||||||
onChange = onParamsChanged,
|
onChange = onParamsChanged,
|
||||||
warnAboutRestart = true,
|
// The same picker the model and permission rows above use: how hard a
|
||||||
|
// session thinks is one kind of setting, whichever provider declares it.
|
||||||
|
choices = ChoiceStyle.Picker,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -649,6 +617,40 @@ fun SessionSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A directory is settled when the process is spawned, so moving means ending it. Said here
|
||||||
|
// rather than under the field, which is the rule the whole screen follows -- see
|
||||||
|
// [RestartDialog].
|
||||||
|
askedCwd?.let { chosen ->
|
||||||
|
RestartDialog(
|
||||||
|
title = "Move to $chosen?",
|
||||||
|
text =
|
||||||
|
"This stops the session's process. It starts again in the new directory with " +
|
||||||
|
"the next message, or with Start.",
|
||||||
|
confirm = "Move",
|
||||||
|
onConfirm = {
|
||||||
|
askedCwd = null
|
||||||
|
moveCwd()
|
||||||
|
},
|
||||||
|
onDismiss = { askedCwd = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// The same cost for the same reason: the CLI reads the level when it launches, and has no
|
||||||
|
// control request for changing one.
|
||||||
|
askedEffort?.let { chosen ->
|
||||||
|
RestartDialog(
|
||||||
|
title = "Think $chosen?",
|
||||||
|
text =
|
||||||
|
"This stops the session's process. It starts again with the next message, or " +
|
||||||
|
"with Start.",
|
||||||
|
confirm = "Change",
|
||||||
|
onConfirm = {
|
||||||
|
askedEffort = null
|
||||||
|
setEffort(chosen.takeIf { it != DEFAULT_EFFORT })
|
||||||
|
},
|
||||||
|
onDismiss = { askedEffort = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -280,7 +280,8 @@ fun SpawnScreen(
|
|||||||
specs = current?.params.orEmpty(),
|
specs = current?.params.orEmpty(),
|
||||||
values = params,
|
values = params,
|
||||||
onChange = { params = it },
|
onChange = { params = it },
|
||||||
warnAboutRestart = false,
|
// Chips, like every other choice on this form.
|
||||||
|
choices = ChoiceStyle.Chips,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Every session whose tools act on files needs one, which is both kinds that have
|
// Every session whose tools act on files needs one, which is both kinds that have
|
||||||
|
|||||||
Reference in new issue
Block a user