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,57 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
// How long is left in a usage window. Shared by the session bar and the usage screen: the
|
||||
// arithmetic is the same in both and only the sentence around it differs, so everything here
|
||||
// returns the span or the state on its own and leaves the wording to the caller.
|
||||
|
||||
/** "1d 4h", "3h 12m", "12m" -- the span alone, with no leading or trailing words. */
|
||||
fun formatSpan(until: Duration): String =
|
||||
when {
|
||||
until.toHours() >= 24 -> "${until.toDays()}d ${until.toHours() % 24}h"
|
||||
until.toHours() > 0 -> "${until.toHours()}h ${until.toMinutes() % 60}m"
|
||||
else -> "${until.toMinutes()}m"
|
||||
}
|
||||
|
||||
/**
|
||||
* What is known about when a usage window ends.
|
||||
*
|
||||
* Three answers rather than a nullable duration, because two of them shared `null` and they are not
|
||||
* the same thing at all. A window the server sent no reset time for is one that is **not running**:
|
||||
* the five-hour window is anchored to the block it started in, so between sessions there is nothing
|
||||
* counting down and the API says so by omitting the field -- measured against a live response on
|
||||
* 2026-08-31, where the five-hour window's reset was exactly five hours after the moment work
|
||||
* resumed. A timestamp that did arrive and could not be read is the genuinely unknown case, and it
|
||||
* is the only one worth those words.
|
||||
*
|
||||
* Collapsing them put "reset time unknown" on the session bar for a machine behaving perfectly, on
|
||||
* the one row somebody reads before starting something big -- and the usage dialog, looking at the
|
||||
* same field, quietly drew nothing. Two rules for one missing value; this is the rule.
|
||||
*/
|
||||
sealed class WindowEnd {
|
||||
/** No reset time was sent, so nothing is running in this window. Not a failure to find out. */
|
||||
data object NotRunning : WindowEnd()
|
||||
|
||||
/** A timestamp arrived and could not be read. The one case that is actually unknown. */
|
||||
data object Unreadable : WindowEnd()
|
||||
|
||||
/** How long is left. Negative once the window is past, which each caller words for itself. */
|
||||
data class Ends(val until: Duration) : WindowEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* [resetsAt] as the server sent it -- absent, unreadable, or a moment -- against [now].
|
||||
*
|
||||
* [now] is a parameter rather than read here so a caller can drive it from state and have the
|
||||
* countdown recompute on its own schedule.
|
||||
*/
|
||||
fun windowEnd(resetsAt: String?, now: OffsetDateTime): WindowEnd {
|
||||
if (resetsAt == null) return WindowEnd.NotRunning
|
||||
return try {
|
||||
WindowEnd.Ends(Duration.between(now, OffsetDateTime.parse(resetsAt)))
|
||||
} catch (_: Exception) {
|
||||
WindowEnd.Unreadable
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user