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:
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 },
|
||||
|
||||
Reference in new issue
Block a user