Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions android/samples/mobile-2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,73 @@ An Android Jetpack Compose chat client that connects to a TypeAgent agent-server
- Incremental assistant response streaming into a single bubble per `requestId`, honouring
the SDK's `DisplayAppendMode` (`inline`, `block`, `temporary`, `step`) and
`DisplayMessageKind` styling the same way the Electron shell does
- Chat history that survives both configuration changes and process death, and
resumes the same server-side conversation (see
[Conversation persistence](#conversation-persistence))
- DevTunnel authentication via `X-Tunnel-Authorization` header
- Build-time configuration via environment variables and `BuildConfig`

## Conversation persistence

The chat conversation is owned by a `ViewModel`, so rotation, theme, font-scale
and locale changes no longer tear down the socket and the transcript.

A `ViewModel` dies with its process though, which Android does routinely once the
app is backgrounded. The transcript is therefore mirrored to `SharedPreferences`
by `ConversationStore` and restored on the next start, capped at the most recent
`ConversationSerializer.MAX_PERSISTED_MESSAGES` messages. The server cannot fill
this gap for this client: it reads no display history, so the client has to own
its own transcript.

The joined `conversationId` is stored alongside the messages and passed back into
`joinConversation` as a connect option on the next launch, so the client resumes
the exact conversation the transcript belongs to rather than landing on the
server's default one. If the server no longer has that conversation it answers
`Conversation not found`; the join then falls back to the default conversation
once and the orphaned transcript is dropped from both screen and disk. Every
other join failure - transport, tunnel auth - still surfaces as a connection
error, so an outage cannot silently move the user into a different conversation.

> **Terminology.** This is a *conversation* (user-facing identity and chat
> history), not a dispatcher *session* (configuration, caches, agent state).
> The `SharedPreferences` file is still named `typeagent_chat_session.xml`
> because that name is pinned in the backup rules and already exists on devices;
> renaming it would orphan stored transcripts.

### What is stored, and for how long

Everything lives in one private `SharedPreferences` file inside the app's own
sandbox (`typeagent_chat_session.xml`), readable only by this app. Nothing is
written to shared or external storage.

Two independent limits keep it from growing without end:

| Limit | Constant | Effect |
|---|---|---|
| Size | `MAX_PERSISTED_MESSAGES` (200) | Only the newest 200 messages are kept. A full 200-message transcript measures ~59 KB. |
| Age | `MAX_MESSAGE_AGE_MILLIS` (30 days) | Messages older than the window are deleted, including while the app is not running. |

Retention runs on both save and load. Because a load only *filters* what it
reads, a read that drops anything immediately rewrites the file, so expired
messages are erased rather than merely hidden. Expiry is applied to what is
stored, not to what is already on screen: messages already visible stay for the
rest of the conversation rather than disappearing mid-chat.

Saving is debounced (`ChatViewModel.SAVE_DEBOUNCE_MS`). `SharedPreferences`
rewrites its entire file on every commit and the message list re-emits on every
streaming chunk, so an undebounced save would rewrite the whole blob dozens of
times per reply.

The transcript is excluded from Android's Auto Backup (`backup_rules.xml` and
`data_extraction_rules.xml`), so conversations are never uploaded to the user's
cloud account. A direct device-to-device transfer does carry it, since that
copies straight to the new phone without a cloud round trip.

**Clear chat** in the header removes the transcript from both the screen and disk
after a confirmation. It is a client-side reset only, matching `@clear` on the
other TypeAgent canvases: the conversation itself is untouched, so the agent
keeps its memory and the next launch resumes the same conversation.

## Client-hosted Android agent

After joining a conversation, the app registers `androidDevice` as a
Expand Down
1 change: 1 addition & 0 deletions android/samples/mobile-2/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.commonmark)
implementation(libs.squareup.okhttp)
testImplementation(libs.json)
Expand Down
Loading
Loading