Ask a provider for its models, and let the app update its CLI

The Claude model list was four words in machines.rs, copied into every
machine's config.ron at discovery -- so a model the CLI had gained was
unreachable from the phone, which is how Opus 5.5 came to be invisible.
Every catalog is now read from the provider itself when a picker opens:
Claude Code over its control channel (control_request{subtype:list_models},
the same channel the driver sends set_model down, since the CLI has no
listing command), Codex over app-server, llama.cpp from the GGUFs on its
machine. Echo is the one case config.ron still answers, having nothing to
ask. A row the CLI marks disabled -- a model the installed version is too
old to run -- is dropped rather than offered as a chip that fails.

The reading of each catalog lives in that driver's own module, and
provider_models is one line per driver: core code says which driver to ask
and never what an answer looks like. parse_codex_models moved out for the
same reason.

Beside it, a provider now reports the version of its program and can be
told to update it. The version comes from --version on its own machine,
read from whichever stream carried it but only from a run that exited 0 --
llama-server prints its version to stderr, and so does "command not found".
It is never compared against a latest release, which nothing here can know.
Update runs the driver's own updater, or an updateCommand the config file
names for an install those will not touch; its whole output comes back,
because every install on these machines is package-managed and the
updater's refusal is the sentence worth reading. Nothing on its stdin, so a
password prompt fails rather than hangs.

A catalog that cannot be read no longer fails the whole provider view: that
is exactly the provider somebody came there to update, and refusing the
screen took the version and the Update button away with it.

Verified against a scratch server and on the emulator: the live catalog
(1.06s, disabled row absent), all four providers' versions including the
unknown state, the confirmation and output dialogs, a configured command
returning in 87ms with its stdin closed, and Update correctly disabled for
llama.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-23 18:32:51 -04:00
1 parent 53fc59a946
commit 7371af8e36
14 files changed
+773 -135

No files matched your search

@@ -1,6 +1,7 @@
package com.example.aiapp
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -73,6 +74,10 @@ fun ProviderScreen(
var busy by remember { mutableStateOf<String?>(null) }
var actionError by remember { mutableStateOf<String?>(null) }
var confirmingDelete by remember { mutableStateOf<ProviderModel?>(null) }
var confirmingUpdate by remember { mutableStateOf(false) }
// What the updater said, kept until it is read: it is the whole point of pressing the button —
// an updater that declines names what to do instead — and there is nowhere else to see it.
var updateSaid by remember { mutableStateOf<String?>(null) }
// The machine's own models and what is being fetched onto it. Only for a provider that serves
// files off that machine's disk -- everything else names its models rather than holding them,
@@ -142,6 +147,34 @@ fun ProviderScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
// Under the program it is the version of, and above the control that changes it:
// the provider's own binary, as opposed to everything below, which is models. A
// kind with no program to ask draws neither.
view.version?.let { version ->
Text(
when (version) {
is ProviderVersion.Known -> version.text
// Words, not a colour: there is no way to tell a version this app
// failed to read from one that happens to look odd.
is ProviderVersion.Unknown -> "Version unknown — ${version.why}"
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
// Its own row rather than beside the line above, which is a short string
// when the version is known and a sentence wrapping to four lines when it is
// not — and the second is the state this button matters most in.
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
// Disabled rather than absent, so a provider nothing here can update says
// so instead of leaving a reader to wonder where the control went.
TextButton(
onClick = { confirmingUpdate = true },
enabled = view.updatable && busy == null,
) {
Text("Update")
}
}
}
Spacer(Modifier.height(12.dp))
actionError?.let {
Text(it, color = MaterialTheme.colorScheme.error)
@@ -183,6 +216,19 @@ fun ProviderScreen(
Spacer(Modifier.height(8.dp))
}
}
// Where the models would be, because that is what is missing. It is not the
// whole screen's failure: the version above it was read, and the control that
// might fix this is up there too.
view.modelsError?.let { failure ->
item("models-failed") {
Text(
"Models unavailable: $failure",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
Spacer(Modifier.height(8.dp))
}
}
machineModels.actionError?.let { failure ->
item("models-error") {
Text(failure, color = MaterialTheme.colorScheme.error)
@@ -278,6 +324,56 @@ fun ProviderScreen(
)
}
if (confirmingUpdate) {
AlertDialog(
onDismissRequest = { confirmingUpdate = false },
title = {
Text("Update ${(state as? LoadState.Loaded)?.value?.name ?: "this provider"}?")
},
text = {
Text(
"The program is updated on ${(state as? LoadState.Loaded)?.value?.machine ?: "its machine"}, " +
"which every session there uses. Sessions already running keep the version they " +
"started with; the next one to start gets the new one. Nothing here can answer a " +
"password prompt, so an update needing one stops rather than waits."
)
},
confirmButton = {
TextButton(
onClick = {
confirmingUpdate = false
act("Updating…") {
updateSaid = updateProvider(settings, machineId, provider).said
}
}
) {
Text("Update")
}
},
dismissButton = {
TextButton(onClick = { confirmingUpdate = false }) { Text("Cancel") }
},
)
}
updateSaid?.let { said ->
AlertDialog(
onDismissRequest = { updateSaid = null },
title = { Text("The updater said") },
text = {
// Its own words, monospace and scrollable: this is a program's output, and the
// sentence that matters is as often "use your package manager" as it is a version.
Text(
said,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
modifier = Modifier.verticalScroll(rememberScrollState()),
)
},
confirmButton = { TextButton(onClick = { updateSaid = null }) { Text("Done") } },
)
}
if (confirmingStop) {
AlertDialog(
onDismissRequest = { confirmingStop = false },