A pure-native Android client for the xbot server, built with Kotlin + Jetpack Compose.
It talks to an unmodified xbot server over the existing web surface:
POST /api/*— auth, sessions, history, send/cancel, AskUser, uploadsPOST /api/rpc— the existing 137-method RPC table (settings, active progress, pending AskUser, …)GET /api/sse— live stream (text, structured progress, tool state, AskUser, queue, session state)
No server-side RPC or protocol changes are required.
Early MVP. Implemented:
- Username/password login (cookie session, persisted with OkHttp
CookieJar) - Silent re-login after server restarts (credentials in Android Keystore AES-GCM)
- Session list from
/api/session-tree, running badge, polling refresh - Create / rename / delete sessions
- Chat history with turn/iteration grouping
- Live SSE streaming with
Last-Event-IDresume, heartbeat watchdog, exponential backoff - Streaming deltas + checkpoints (
stream_content/stream_delta) - Tool cards (status, elapsed, args, markdown output)
- Reasoning blocks, native Markdown rendering, code blocks
- AskUser dialog (options, multi-select, free text, cancel)
- Send, queue while busy, stop generation, attachments (multipart upload)
- Foreground notification service: completion / AskUser notifications while backgrounded
- Light/dark Material 3 theme
- Notifications are not system push. xbot has no FCM/WebPush transport. This app keeps one SSE connection in a foreground service (Android 8+ mandatory ongoing notification). OEM battery managers and Android 15 foreground-service limits can still interrupt it.
- Plugin / GenUI views are not rendered. The web plugin runtime is JavaScript/React.
Tool output and normal Markdown are native;
ui_mode=genuicontent is shown as text. - No offline cache yet (history is fetched from the server).
- No settings/plugin-market/terminal parity with the web UI.
- API contract is internal and unversioned; pin the server version you test against.
- Android 8.0 (API 26) or newer
- An xbot server reachable from the phone (
web.enable=true) - A web account on that server (the first account can be created from this app on a fresh server)
The repository includes a Gradle wrapper. If ./gradlew is not executable:
chmod +x gradlew
./gradlew :app:assembleDebugThe debug APK is at app/build/outputs/apk/debug/app-debug.apk.
Prerequisites: JDK 17+ (tested with Temurin 21) and an Android SDK with
platforms;android-35 / build-tools;35.0.0. Point Gradle at the SDK with
local.properties:
sdk.dir=/path/to/Android/SdkUnit tests (pure JVM state-machine tests):
./gradlew :app:testDebugUnitTest- Enable the web channel in
~/.xbot/config.json:
{
"web": {
"enable": true,
"host": "0.0.0.0",
"port": 16000
}
}- Start xbot (
xbot-cli serveor thexbot-serverservice). - In the app, enter
http://<server-lan-ip>:16000, then log in or create the first account.
For public networks, put xbot behind HTTPS and set web.public_url to the HTTPS origin;
the session cookie is only marked Secure when public_url starts with https://.
The app currently allows cleartext HTTP for LAN use (usesCleartextTraffic="true").
app/src/main/java/dev/xbot/android/
data/ wire models, REST/RPC client, cookie jar, SSE client, prefs
chat/ pure chat reducer + ChatViewModel
sessions/ session list view model/screen
auth/ login view model/screen
service/ foreground SSE notification service, boot receiver, visibility
ui/ Compose navigation, chat/settings screens, markdown/tool components
The core of the client is chat/ChatReducer.kt — intentionally a pure function of
(state, event). It mirrors the important parts of the web client's state machine:
one turn per turn_id, iterations merged by number, stream checkpoints replace and
deltas append, and turn-ending signals never discard rendered content.
Do not commit local secrets or machine-specific paths:
keystore.propertiesandkeystore/(release signing key + passwords)local.properties(Android SDK path)dist/(release APKs +latest.json)tools/(local JDK / Android SDK / Gradle downloads)
Create your signing config from the template:
cp keystore.properties.example keystore.properties
# then fill in storePassword / keyPasswordBoth channels publish the same signed APK plus a JSON manifest; the app only
compares versionCode and verifies sha256 + the signer certificate, so it can
switch channels in place without losing data.
| Channel | Manifest URL | Produced by |
|---|---|---|
| stable | https://github.com/ai-pivot/xbot-android/releases/latest/download/latest.json |
pushing a v* tag (release.yml) |
| dev | https://github.com/ai-pivot/xbot-android/releases/download/dev/latest-dev.json |
every push to master (dev.yml, lands on the dev prerelease) |
Pick one in Settings → 更新 → 更新清单地址 (nothing is hard-coded in the source), then tap 检查更新 once to persist it; afterwards the app re-checks every 6 hours.
Releasing a stable version:
# 1. bump version.properties (versionCode must exceed the newest dev build —
# release.yml fails with the exact number to use if it does not)
# 2. commit, merge to master
git tag v0.3.3 && git push origin v0.3.3dev.yml numbers dev builds as max(stable, previous dev) + 1, so a later stable
release always outranks every dev build — switching a device from dev back to
stable is a normal upgrade, not a reinstall. A manifest older than the install
cannot be installed by Android; the update card says so instead of showing
"已是最新".
Update notes come from CHANGELOG.md: the workflow extracts only the
## [x.y.z] section matching the release.
MIT — see LICENSE.