Merge branch 'main' of git.arirex.me:iris/ai-app
# Conflicts: # AGENTS.md # PLAN.md # app/androidApp/src/main/kotlin/com/example/aiapp/SessionUsageBar.kt # app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt # server/src/config.rs # server/src/main.rs # server/src/routes.rs # server/src/session/echo.rs # server/src/session/llama.rs # server/src/session/transport.rs # server/src/ssh.rs # server/src/usage.rs
This commit is contained in:
commit
3c0214ece8
94 files changed
+8299
-9454
No files matched your search
@@ -37,7 +37,7 @@ import kotlinx.coroutines.withContext
|
||||
* The spawn screen: what to run, where to run it, and the per-kind fields.
|
||||
*
|
||||
* Providers and hosts both come from the server, so adding either to its config.ron shows up here
|
||||
* with no app rebuild -- and because they are independent, any provider can be sent to any host.
|
||||
* with no app rebuild.
|
||||
*/
|
||||
@Composable
|
||||
fun SpawnScreen(
|
||||
@@ -46,29 +46,24 @@ fun SpawnScreen(
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
// What the form is made of, and whether we have it yet. A failure here
|
||||
// is not the same as a server with nothing to offer, so it must not
|
||||
// reach the pickers as empty lists -- see LoadState.
|
||||
// What the form is made of, and whether we have it yet. A failure here is not the same as a
|
||||
// server with nothing to offer, so it must not reach the pickers as empty lists.
|
||||
var options by remember { mutableStateOf<LoadState<List<Setup>>>(LoadState.Loading) }
|
||||
|
||||
// Setup first, then one of its providers. Choosing a setup can
|
||||
// invalidate the provider, so the provider is stored by name and
|
||||
// resolved against the current setup rather than held as an object
|
||||
// that could outlive the list it came from.
|
||||
// Setup first, then one of its providers. Choosing a setup can invalidate the provider, so the
|
||||
// provider is stored by name and resolved against the current setup rather than held as an
|
||||
// object that could outlive the list it came from.
|
||||
var setupName by remember { mutableStateOf<String?>(null) }
|
||||
var providerName by remember { mutableStateOf<String?>(null) }
|
||||
var title by remember { mutableStateOf("") }
|
||||
var model by remember { mutableStateOf("") }
|
||||
var cwd by remember { mutableStateOf("") }
|
||||
// "auto" rather than "manual": on a phone every ask is a round trip to
|
||||
// a question card, and answering "allow Bash?" dozens of times per task
|
||||
// is what this app exists to avoid. Manual stays one tap away for a
|
||||
// session that warrants it.
|
||||
// "auto" rather than "manual": on a phone every ask is a round trip to a question card, and
|
||||
// answering "allow Bash?" dozens of times per task is what this app exists to avoid.
|
||||
var permissionMode by remember { mutableStateOf("auto") }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
// Only the spawn's own failure. The fetch's lives in `options`: this
|
||||
// one leaves a filled-in form worth keeping, and that one leaves
|
||||
// nothing to fill in.
|
||||
// Only the spawn's own failure. The fetch's lives in `options`: this one leaves a filled-in
|
||||
// form worth keeping, and that one leaves nothing to fill in.
|
||||
var spawnError by remember { mutableStateOf<String?>(null) }
|
||||
// The models on the *chosen machine*, for a llama provider to choose between. Kept separate
|
||||
// from the setups: a Claude session needs none, so failing to list them must not stop the
|
||||
@@ -103,10 +98,9 @@ fun SpawnScreen(
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Nothing below is fillable until the options are here, and a
|
||||
// failure to fetch them leaves no form worth showing -- so this
|
||||
// reports and stops, rather than offering empty pickers under an
|
||||
// error message.
|
||||
// Nothing below is fillable until the options are here, and a failure to fetch them leaves
|
||||
// no form worth showing -- so this reports and stops, rather than offering empty pickers
|
||||
// under an error message.
|
||||
val setups =
|
||||
when (val state = options) {
|
||||
is LoadState.Loading -> {
|
||||
@@ -132,10 +126,9 @@ fun SpawnScreen(
|
||||
.getOrDefault(emptyList())
|
||||
}
|
||||
val current = setup?.providers?.firstOrNull { it.name == providerName }
|
||||
// Only the Claude CLI has models, a working directory and
|
||||
// permission modes; keying the extra fields on the kind rather
|
||||
// than the provider name keeps a second Claude provider from
|
||||
// needing anything here.
|
||||
// Only the Claude CLI has models, a working directory and permission modes; keying the
|
||||
// extra fields on the kind rather than the provider name keeps a second Claude provider
|
||||
// from needing anything here.
|
||||
val isClaude = current?.kind == "claude_cli"
|
||||
val isLlama = current?.kind == "llama_cpp"
|
||||
|
||||
@@ -146,9 +139,9 @@ fun SpawnScreen(
|
||||
selected = setupName,
|
||||
onSelect = { name ->
|
||||
setupName = name
|
||||
// The provider list changes with the machine, so a name
|
||||
// carried over from the previous one would be a selection
|
||||
// that isn't in the picker. Take that machine's first.
|
||||
// The provider list changes with the machine, so a name carried over from the
|
||||
// previous one would be a selection that isn't in the picker. Take that machine's
|
||||
// first.
|
||||
providerName =
|
||||
setups.firstOrNull { it.name == name }?.providers?.firstOrNull()?.name
|
||||
},
|
||||
@@ -159,13 +152,13 @@ fun SpawnScreen(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
// The address belongs to the setup above it, not to the
|
||||
// provider label below; without this they read as one block.
|
||||
// The address belongs to the setup above it, not to the provider label below; without
|
||||
// this they read as one block.
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// Only what this machine actually has. A setup with none says so
|
||||
// rather than showing an empty row that reads as a failure.
|
||||
// Only what this machine actually has. A setup with none says so rather than showing an
|
||||
// empty row that reads as a failure.
|
||||
if (setup != null && setup.providers.isEmpty()) {
|
||||
Text(
|
||||
"\"${setup.name}\" has no providers configured.",
|
||||
@@ -193,9 +186,8 @@ fun SpawnScreen(
|
||||
|
||||
if (isLlama) {
|
||||
// A llama session names one of the models on the machine it will run on, so the
|
||||
// choice is that list rather than free text -- there is nothing sensible to type
|
||||
// here, and a name that is not on that machine's disk is a session that cannot
|
||||
// start.
|
||||
// choice is that list rather than free text -- a name that is not on that machine's
|
||||
// disk is a session that cannot start.
|
||||
if (models.isEmpty()) {
|
||||
Text(
|
||||
"No models on ${setup?.name ?: "this machine"}. The Models screen downloads " +
|
||||
@@ -206,9 +198,8 @@ fun SpawnScreen(
|
||||
} else {
|
||||
ChipGroup(
|
||||
label = "Model",
|
||||
// The file, not the whole key: the repository is the
|
||||
// same for every quantisation of a model, so the file
|
||||
// name is what tells two of them apart.
|
||||
// The file, not the whole key: the repository is the same for every
|
||||
// quantisation of a model, so the file name is what tells two of them apart.
|
||||
options = models.map { it.file },
|
||||
selected = models.firstOrNull { it.key == modelKey }?.file,
|
||||
onSelect = { file -> modelKey = models.first { it.file == file }.key },
|
||||
@@ -290,13 +281,9 @@ fun SpawnScreen(
|
||||
withContext(Dispatchers.IO) {
|
||||
spawnSession(
|
||||
settings,
|
||||
// The id, not the label: labels are
|
||||
// editable and the server resolves by
|
||||
// id.
|
||||
// Non-null here: `chosen` came from
|
||||
// `setup`'s own provider list, so
|
||||
// reaching this point proves there was
|
||||
// a setup to take it from.
|
||||
// The id, not the label: labels are editable and the server
|
||||
// resolves by id. Non-null here, since `chosen` came from
|
||||
// `setup`'s own provider list.
|
||||
setup = setup.id,
|
||||
provider = chosen.name,
|
||||
title = title.trim(),
|
||||
@@ -304,9 +291,8 @@ fun SpawnScreen(
|
||||
if (isLlama) modelKey else model.trim().takeIf { isClaude },
|
||||
cwd = cwd.trim().takeIf { isClaude },
|
||||
permissionMode = permissionMode.takeIf { isClaude },
|
||||
// Sent only when set, so blank means
|
||||
// "whatever llama.cpp does by default"
|
||||
// rather than a zero.
|
||||
// Sent only when set, so blank means "whatever llama.cpp does
|
||||
// by default" rather than a zero.
|
||||
params =
|
||||
buildMap {
|
||||
if (isLlama) {
|
||||
|
||||
Reference in new issue
Block a user