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:
iris committed 2026-09-04 17:56:50 -04:00
commit 3c0214ece8
94 files changed
+8299 -9454

No files matched your search

@@ -58,8 +58,8 @@ fun SetupsScreen(settings: ServerSettings, reloadToken: Int) {
LaunchedEffect(reloadToken) { reload() }
Column(Modifier.fillMaxSize().padding(16.dp)) {
// The heading and Back are the tab row's now; adding a machine is this tab's own work
// and stays with the list it adds to.
// The heading and Back are the tab row's now; adding a machine is this tab's own work and
// stays with the list it adds to.
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
TextButton(onClick = { adding = true }) { Text("Add machine") }
}
@@ -200,9 +200,8 @@ private fun SetupCard(
Column(Modifier.padding(12.dp)) {
Text(setup.name, style = MaterialTheme.typography.titleSmall)
Text(
// Not "this machine": the seeded setup is *called* that,
// and the card read "this machine / this machine". The
// line has to say something the name cannot also be.
// Not "this machine": the seeded setup is *called* that, and the card read "this
// machine / this machine".
setup.address ?: "runs where the backend does",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -277,9 +276,8 @@ private fun AddSetupDialog(
OutlinedTextField(
value = address,
onValueChange = { address = it },
// Just the shape. What a blank one means is said once, in the text above
// this form -- repeating it here wrapped the label onto a second line and
// made this field taller than the two beside it for no information.
// Just the shape. What a blank one means is said once, in the text above this
// form -- repeating it here wrapped the label onto a second line.
label = { Text("user@host[:port]") },
singleLine = true,
)
@@ -290,8 +288,7 @@ private fun AddSetupDialog(
singleLine = true,
)
// Where a file attached from the phone lands on that machine. Blank means the
// session's own directory, which is what most people want and what needs no
// path typed on a phone.
// session's own directory, which is what most people want.
OutlinedTextField(
value = attachmentsDir,
onValueChange = { attachmentsDir = it },
@@ -319,9 +316,8 @@ private fun AddSetupDialog(
},
dismissButton = {
Row {
// Tried before saving, so a wrong address or an
// unauthorised key is caught while this form is still on
// screen rather than at the first spawn.
// Tried before saving, so a wrong address or an unauthorised key is caught while
// this form is still on screen rather than at the first spawn.
TextButton(
enabled = !testing,
onClick = {
@@ -387,14 +383,13 @@ private fun RenameDialog(setup: Setup, onDismiss: () -> Unit, onRename: (String)
/**
* Splits `user@host:port` into its two halves, with the port left null when none was typed.
*
* One field rather than two because that is how an address is written and read everywhere else --
* and because a port that is almost always 22 does not deserve a box of its own on a phone
* keyboard. Null rather than 22: the backend already decides the default, and writing 22 here would
* put a second answer to that question in a second place.
* One field rather than two because that is how an address is written and read everywhere else, and
* because a port that is almost always 22 does not deserve a box of its own on a phone keyboard.
* Null rather than 22: the backend already decides the default.
*
* A colon only means "port" when it can. A bracketed IPv6 literal is unwrapped as ssh writes it,
* `[::1]:22`; a bare `::1` keeps every colon, because an address with several is an address, not an
* address and a port. So the rule is: brackets, or exactly one colon followed by digits.
* `[::1]:22`; a bare `::1` keeps every colon. So the rule is: brackets, or exactly one colon
* followed by digits.
*/
private fun splitHostAndPort(typed: String): Pair<String, Int?> {
if (typed.startsWith("[")) {