Phase 1 app: session list, session screen, spawn, QR enrollment over pinned TLS
Compose app mirroring local-updater's stack (single :androidApp module, pinned CA, HttpURLConnection transport) plus what this app needs on top: a bearer token sealed with an Android Keystore AES-GCM key, an aiapp://enroll intent filter so scanning the server's terminal QR with the stock camera enrolls the phone with no QR library, an SSE client that resumes by transcript cursor, and a transcript renderer folding the common event model into user bubbles, streaming text, collapsible tool cards, and answerable question cards. Verified on the tdep emulator against the real server: enrollment deep link, list, spawn, streamed echo turn, question answer round trip, tool card expansion, adjustResize keyboard behavior. Build is warning-clean (compose.* accessors replaced with direct dependencies). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
967fc814ab
commit
213bc72b64
24 files changed
+2086
No files matched your search
@@ -0,0 +1,63 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
// The common event model, mirrored from server/src/session/driver.rs --
|
||||
// the app renders purely from this stream (replayed from the transcript by
|
||||
// cursor, then live), so there is no separate "load history" shape to keep
|
||||
// in sync with it.
|
||||
|
||||
/** One transcript line: the event plus its resume cursor and time. */
|
||||
data class SeqEvent(val seq: Long, val ts: Double, val event: SessionEvent)
|
||||
|
||||
sealed class SessionEvent {
|
||||
data class UserMessage(val text: String) : SessionEvent()
|
||||
data class AssistantText(val delta: String) : SessionEvent()
|
||||
data class ToolStart(val id: String, val tool: String, val input: String) : SessionEvent()
|
||||
data class ToolUpdate(val id: String, val output: String) : SessionEvent()
|
||||
data class ToolEnd(val id: String, val output: String) : SessionEvent()
|
||||
data class Image(val ref: String) : SessionEvent()
|
||||
data class Question(val id: String, val prompt: String, val options: List<String>) : SessionEvent()
|
||||
data class Answered(val id: String, val answer: String) : SessionEvent()
|
||||
data class Status(val state: String) : SessionEvent()
|
||||
data class UsageDelta(val tokens: Long) : SessionEvent()
|
||||
data class Error(val message: String) : SessionEvent()
|
||||
|
||||
/**
|
||||
* An event type this app build doesn't know -- a newer server. Kept
|
||||
* (not thrown) so one new event kind degrades to a placeholder row
|
||||
* instead of killing the stream.
|
||||
*/
|
||||
data class Unknown(val type: String) : SessionEvent()
|
||||
}
|
||||
|
||||
fun parseSeqEvent(json: String): SeqEvent {
|
||||
val body = JSONObject(json)
|
||||
val event = when (val type = body.getString("type")) {
|
||||
"userMessage" -> SessionEvent.UserMessage(body.getString("text"))
|
||||
"assistantText" -> SessionEvent.AssistantText(body.getString("delta"))
|
||||
"toolStart" -> SessionEvent.ToolStart(
|
||||
id = body.getString("id"),
|
||||
tool = body.getString("tool"),
|
||||
// Kept as raw JSON text: the input shape is the tool's own
|
||||
// business, and the UI only ever shows it verbatim.
|
||||
input = body.get("input").toString(),
|
||||
)
|
||||
"toolUpdate" -> SessionEvent.ToolUpdate(body.getString("id"), body.getString("output"))
|
||||
"toolEnd" -> SessionEvent.ToolEnd(body.getString("id"), body.getString("output"))
|
||||
"image" -> SessionEvent.Image(body.getString("ref"))
|
||||
"question" -> SessionEvent.Question(
|
||||
id = body.getString("id"),
|
||||
prompt = body.getString("prompt"),
|
||||
options = body.getJSONArray("options").let { options ->
|
||||
(0 until options.length()).map { options.getString(it) }
|
||||
},
|
||||
)
|
||||
"answered" -> SessionEvent.Answered(body.getString("id"), body.getString("answer"))
|
||||
"status" -> SessionEvent.Status(body.getString("state"))
|
||||
"usageDelta" -> SessionEvent.UsageDelta(body.getLong("tokens"))
|
||||
"error" -> SessionEvent.Error(body.getString("message"))
|
||||
else -> SessionEvent.Unknown(type)
|
||||
}
|
||||
return SeqEvent(seq = body.getLong("seq"), ts = body.getDouble("ts"), event = event)
|
||||
}
|
||||
Reference in new issue
Block a user