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:
commit
b172c464ea
100 files changed
+31795
No files matched your search
@@ -0,0 +1,282 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import dev.snipme.highlights.model.SyntaxTheme
|
||||
|
||||
/**
|
||||
* Catppuccin Mocha, as published in `catppuccin/palette`.
|
||||
*
|
||||
* Named rather than used as literals at the point of need, so the mapping below reads as the
|
||||
* decision it is -- "a card is Surface 0" -- and so a value can be checked against the upstream
|
||||
* palette without reading the layout that uses it.
|
||||
*/
|
||||
private object Mocha {
|
||||
val Rosewater = Color(0xFFF5E0DC)
|
||||
val Mauve = Color(0xFFCBA6F7)
|
||||
val Red = Color(0xFFF38BA8)
|
||||
val Peach = Color(0xFFFAB387)
|
||||
val Yellow = Color(0xFFF9E2AF)
|
||||
val Green = Color(0xFFA6E3A1)
|
||||
val Teal = Color(0xFF94E2D5)
|
||||
val Sky = Color(0xFF89DCEB)
|
||||
val Blue = Color(0xFF89B4FA)
|
||||
val Lavender = Color(0xFFB4BEFE)
|
||||
val Text = Color(0xFFCDD6F4)
|
||||
val Subtext0 = Color(0xFFA6ADC8)
|
||||
val Overlay0 = Color(0xFF6C7086)
|
||||
val Surface2 = Color(0xFF585B70)
|
||||
val Surface1 = Color(0xFF45475A)
|
||||
val Surface0 = Color(0xFF313244)
|
||||
val Base = Color(0xFF1E1E2E)
|
||||
val Mantle = Color(0xFF181825)
|
||||
val Crust = Color(0xFF11111B)
|
||||
}
|
||||
|
||||
/**
|
||||
* The app's colour scheme: Catppuccin Mocha mapped onto Material's roles.
|
||||
*
|
||||
* Copied from dev-updater rather than shared, which is a deliberate line: wg-app-link is the *link*
|
||||
* -- the tunnel, the pinned CA, enrollment -- and a palette is not that. The two apps looking alike
|
||||
* is a preference, not a contract, and the moment one wants a different accent the shared version
|
||||
* becomes a thing to fight rather than a thing to use.
|
||||
*
|
||||
* The mapping that matters is the surface ladder. Mocha names its darks in order -- Crust, Mantle,
|
||||
* Base, Surface 0, Surface 1 -- and Material asks for the same thing under different names, so the
|
||||
* page is Base, a component's outlined card stays Base beside it, and a project's card is Surface
|
||||
* 0: one visible step up, which is the whole of what the nesting has to say.
|
||||
*
|
||||
* Accents on this palette are light, so anything filled with one takes Crust for its text rather
|
||||
* than the near-white the roles default to.
|
||||
*/
|
||||
val AiAppColors =
|
||||
darkColorScheme(
|
||||
primary = Mocha.Mauve,
|
||||
onPrimary = Mocha.Crust,
|
||||
primaryContainer = Mocha.Surface1,
|
||||
onPrimaryContainer = Mocha.Mauve,
|
||||
secondary = Mocha.Lavender,
|
||||
onSecondary = Mocha.Crust,
|
||||
secondaryContainer = Mocha.Surface1,
|
||||
onSecondaryContainer = Mocha.Lavender,
|
||||
tertiary = Mocha.Rosewater,
|
||||
onTertiary = Mocha.Crust,
|
||||
background = Mocha.Base,
|
||||
onBackground = Mocha.Text,
|
||||
surface = Mocha.Base,
|
||||
onSurface = Mocha.Text,
|
||||
surfaceVariant = Mocha.Surface0,
|
||||
onSurfaceVariant = Mocha.Subtext0,
|
||||
surfaceContainerLowest = Mocha.Crust,
|
||||
surfaceContainerLow = Mocha.Mantle,
|
||||
surfaceContainer = Mocha.Base,
|
||||
surfaceContainerHigh = Mocha.Surface0,
|
||||
surfaceContainerHighest = Mocha.Surface0,
|
||||
inverseSurface = Mocha.Text,
|
||||
inverseOnSurface = Mocha.Base,
|
||||
inversePrimary = Mocha.Mauve,
|
||||
outline = Mocha.Overlay0,
|
||||
outlineVariant = Mocha.Surface2,
|
||||
error = Mocha.Red,
|
||||
onError = Mocha.Crust,
|
||||
errorContainer = Mocha.Surface1,
|
||||
onErrorContainer = Mocha.Red,
|
||||
scrim = Mocha.Crust,
|
||||
)
|
||||
|
||||
/**
|
||||
* What a session is doing, said in colour.
|
||||
*
|
||||
* Here rather than beside each screen that shows a status. These were separate literals in two
|
||||
* other files -- an amber, a green and a red picked off Material's defaults -- so the same state
|
||||
* was a slightly different colour depending which screen you looked at, and none of them belonged
|
||||
* to this palette at all. A colour that carries meaning is part of the scheme, not a value typed
|
||||
* where it happened to be needed.
|
||||
*/
|
||||
val runningColor: Color
|
||||
@Composable get() = Mocha.Green
|
||||
|
||||
/**
|
||||
* "This went wrong on its own": a session that fell over.
|
||||
*
|
||||
* The scheme's error colour, and deliberately not "the same red as a destructive button" even
|
||||
* though it is the same red. They are the same red for different reasons, and a state is not an
|
||||
* action -- nothing here is a button.
|
||||
*/
|
||||
val failedColor: Color
|
||||
@Composable get() = MaterialTheme.colorScheme.error
|
||||
|
||||
/**
|
||||
* About the session rather than about the task: a command, and the compaction one of them starts.
|
||||
*
|
||||
* Its own colour because it is its own kind of work. Everything else a session does is progress
|
||||
* through what was asked of it; this is the session acting on itself -- rewriting what it
|
||||
* remembers, taking a new name -- and none of it appears in the transcript as an answer to
|
||||
* anything. A reader who has learned that blue means "not stuck, but not replying to you either"
|
||||
* has learned the thing that distinguishes it from a session that has hung.
|
||||
*/
|
||||
val commandColor: Color
|
||||
@Composable get() = Mocha.Blue
|
||||
|
||||
/**
|
||||
* A clear: the conversation taken out of what the session is given.
|
||||
*
|
||||
* Red because of what it does, not because anything went wrong -- somebody asked for this, and a
|
||||
* deliberate choice is not a problem to report. It is the same red as [failedColor] and [stopColor]
|
||||
* for a third reason, which is worth naming rather than collapsing: this is neither a fault nor a
|
||||
* button, it is the mark left where something was taken away. The reader never has to tell the
|
||||
* three apart, because no two of them can appear as the same kind of thing.
|
||||
*/
|
||||
val clearedColor: Color
|
||||
@Composable get() = Mocha.Red
|
||||
|
||||
/** Waiting on a person: a question, a permission, a turn that is theirs. */
|
||||
val awaitingColor: Color
|
||||
@Composable get() = Mocha.Peach
|
||||
|
||||
/** Approaching a limit -- still fine, worth seeing. */
|
||||
val warningColor: Color
|
||||
@Composable get() = Mocha.Yellow
|
||||
|
||||
/**
|
||||
* The fill of a progress bar that is only reporting how far along something is.
|
||||
*
|
||||
* Blue because a bar like this reports a quantity rather than a verdict, and the scheme's primary
|
||||
* made it the loudest thing on a screen the reader opened to do something else. A download, or a
|
||||
* compaction, has no limit to be near: it finishes. Only a bar measuring a *quota* escalates, and
|
||||
* that one is [quotaColor].
|
||||
*/
|
||||
val progressColor: Color
|
||||
@Composable get() = Mocha.Blue
|
||||
|
||||
/**
|
||||
* The fill of a bar measuring how much of a quota is gone: blue, then yellow, then red.
|
||||
*
|
||||
* One function rather than the same `when` written beside each bar, because the whole point of
|
||||
* colouring by consequence is that the reader learns the step once -- two bars showing the same 80%
|
||||
* in different colours teaches nothing except that the colour cannot be trusted. It reads as a
|
||||
* difference in degree, which is all colour can carry: the states that differ in *kind* from this
|
||||
* -- a window nobody could read, a machine that meters nothing -- are said in words elsewhere,
|
||||
* because a reader has no way to tell those from an ordinary low number by colour alone.
|
||||
*
|
||||
* [percent] is the API's own 0-100 rather than a fraction, so callers pass what the server sent
|
||||
* without each converting it first and one of them getting it wrong by a factor of a hundred.
|
||||
*/
|
||||
@Composable
|
||||
fun quotaColor(percent: Double): Color =
|
||||
when {
|
||||
percent >= OVER_LIMIT_PERCENT -> overLimitColor
|
||||
percent >= WARNING_PERCENT -> warningColor
|
||||
else -> progressColor
|
||||
}
|
||||
|
||||
/** Close enough to the limit to be worth seeing before starting something big. */
|
||||
private const val WARNING_PERCENT = 75.0
|
||||
|
||||
/** Close enough that the next turn may be the one that is refused. */
|
||||
private const val OVER_LIMIT_PERCENT = 90.0
|
||||
|
||||
/**
|
||||
* The surface verbatim text sits on: a command, a tool's output, a code block in a reply.
|
||||
*
|
||||
* The darkest value in the palette rather than a step up from the page, and that is the whole point
|
||||
* -- everything else on this screen is somebody's prose, and this is what a machine was handed and
|
||||
* what it said back, character for character. Crust sits *below* Base, so the same colour reads as
|
||||
* one clear step down both on the page, where a reply is drawn, and on a card, where a tool call
|
||||
* is; a tint chosen upwards has to be picked twice and still collides with the card it lands on.
|
||||
* The renderer's default code background was `surfaceVariant`, which is exactly a card's own fill
|
||||
* -- so a code block inside a tool call had no background at all.
|
||||
*
|
||||
* One colour for all three, so "this is verbatim" is learnable once.
|
||||
*/
|
||||
val rawSurface: Color
|
||||
@Composable get() = Mocha.Crust
|
||||
|
||||
/**
|
||||
* Catppuccin Mocha as a syntax theme, for the highlighter used on a tool call's input.
|
||||
*
|
||||
* Here with the rest of the palette rather than beside the code that highlights: a library's own
|
||||
* theme would otherwise be the one surface in the app whose colours came from somewhere else, and
|
||||
* the accents below are the same ones every other coloured thing already uses.
|
||||
*/
|
||||
fun catppuccinSyntax(): SyntaxTheme =
|
||||
SyntaxTheme(
|
||||
key = "catppuccin-mocha",
|
||||
code = Mocha.Text.toArgb(),
|
||||
keyword = Mocha.Mauve.toArgb(),
|
||||
string = Mocha.Green.toArgb(),
|
||||
literal = Mocha.Peach.toArgb(),
|
||||
comment = Mocha.Overlay0.toArgb(),
|
||||
metadata = Mocha.Yellow.toArgb(),
|
||||
multilineComment = Mocha.Overlay0.toArgb(),
|
||||
punctuation = Mocha.Subtext0.toArgb(),
|
||||
mark = Mocha.Sky.toArgb(),
|
||||
)
|
||||
|
||||
/**
|
||||
* A link. Blue is what a link is on every Catppuccin surface, and the one colour to leave alone.
|
||||
*/
|
||||
val linkColor: Color
|
||||
@Composable get() = Mocha.Blue
|
||||
|
||||
/** Past a limit. The scheme's error colour, for the reason [failedColor] gives. */
|
||||
val overLimitColor: Color
|
||||
@Composable get() = MaterialTheme.colorScheme.error
|
||||
|
||||
/**
|
||||
* The composer's buttons, coloured by what pressing one does rather than by where it sits.
|
||||
*
|
||||
* Green makes something happen now, blue makes it happen later, orange takes back what is in
|
||||
* flight, red ends the process. The near-collisions with the states above are deliberate and worth
|
||||
* naming rather than collapsing: [runningColor] is green because a session is working,
|
||||
* [failedColor] is red because one fell over, [awaitingColor] is the same orange because a session
|
||||
* is waiting on somebody -- those are *states*, and these are *actions*. A reader never has to tell
|
||||
* them apart, because nothing here is a state and nothing there is pressable.
|
||||
*/
|
||||
val sendColor: Color
|
||||
@Composable get() = Mocha.Green
|
||||
|
||||
/** Sending while a turn runs: the message waits rather than starting one. See [sendColor]. */
|
||||
val queueColor: Color
|
||||
@Composable get() = Mocha.Blue
|
||||
|
||||
/**
|
||||
* Interrupting the running turn: the work stops and the session stays.
|
||||
*
|
||||
* Orange rather than red because of how much it takes: only what is in flight. The process is still
|
||||
* there holding the conversation, and the next message starts a turn as though nothing had
|
||||
* happened. Red is spent on [stopColor], which is the same button in the same place when what it
|
||||
* would end is the session's process.
|
||||
*/
|
||||
val pauseColor: Color
|
||||
@Composable get() = Mocha.Peach
|
||||
|
||||
/** Ending the session's process -- the one button here that takes something away. */
|
||||
val stopColor: Color
|
||||
@Composable get() = Mocha.Red
|
||||
|
||||
/**
|
||||
* Starting the process again, on the conversation it left.
|
||||
*
|
||||
* The same green as [sendColor] on purpose: both mean "this happens now", and they are never the
|
||||
* same button -- the process button only offers to start when there is nothing running to stop.
|
||||
*/
|
||||
val startColor: Color
|
||||
@Composable get() = Mocha.Green
|
||||
|
||||
/**
|
||||
* A filled button in one of the action colours above.
|
||||
*
|
||||
* The content colour is stated here beside the fill rather than inherited. A semantic colour has to
|
||||
* carry its own contrast: these fills are fixed whatever the surface under them does, so the theme
|
||||
* will not change to rescue a foreground that stops being readable on one of them. Crust is what
|
||||
* every accent on this palette takes, which is the same reason `onPrimary` is Crust above.
|
||||
*/
|
||||
@Composable
|
||||
fun actionButtonColors(fill: Color): ButtonColors =
|
||||
ButtonDefaults.buttonColors(containerColor = fill, contentColor = Mocha.Crust)
|
||||
Reference in new issue
Block a user