ai-app: a phone interface to Claude Code and llama.cpp sessions

A Rust backend that owns the sessions and an Android app that reads them.
The server spawns and adopts CLI processes, normalises everything they emit
into one event model, keeps the transcript, and serves it over pinned TLS on
a WireGuard interface; the phone streams that, replies, sends images, and
imports conversations the machine already has.

`AGENTS.md` is the working guide -- what runs where, what has been measured,
and the faults that were expensive to find. `PLAN.md` is the design record.

History before this point was squashed away. It was a personal project's
running commentary and carried a name and a couple of machine paths that
have no business in a public repository; the tree is what mattered and the
tree is here.
This commit is contained in:
iris committed 2026-08-31 20:29:07 -04:00
commit b172c464ea
100 files changed
+31795

No files matched your search

@@ -0,0 +1,35 @@
package com.example.aiapp
/**
* What a session with no model of its own is called, in the button and in the list it opens.
*
* One constant rather than a literal in each place, because the two have to agree: a picker whose
* options cannot say every state its button can display is one you can leave and not get back to.
* It is also the Claude CLI's own word for "whatever is configured", so choosing it is a request
* the session can act on rather than a name this app made up.
*/
const val DEFAULT_MODEL = "default"
/**
* A model's name as a person reads it.
*
* Providers answer with their own full identifier -- Claude Code resolves `haiku` to
* `claude-haiku-4-5-20251001` and reports that, which is the honest answer to "what is this session
* using" and far too long for a button in a row that also has to hold Stop and Send.
*
* So the two ends that identify nothing are dropped and nothing else is: the vendor prefix, which
* is the same on every model this app can show, and the release date, which distinguishes builds of
* one model rather than one model from another. What is left is the part somebody chose --
* `haiku-4-5` -- and anything that does not look like that is returned untouched, since a name this
* does not recognise is a name it has no business editing.
*
* A display decision, not a correction: the full name is what the session reports and what a reader
* is shown when there is room for it.
*/
fun modelLabel(model: String?): String {
val name = model?.takeIf { it.isNotBlank() } ?: return DEFAULT_MODEL
return name.removePrefix("claude-").replace(DATED_SUFFIX, "")
}
/** A trailing `-YYYYMMDD`, which is how these identifiers carry their release date. */
private val DATED_SUFFIX = Regex("""-\d{8}$""")