on master help sign in
crush-android/README.md
114 lines 4.9 kB history raw
1# crush-android
3An Android client for the [crush](https://github.com/charmbracelet/crush) AI coding
4assistant's server mode. Built with Kotlin and Jetpack Compose.
6## How it works
8Crush has a client/server architecture. Running `crush server --host 0.0.0.0:8080`
9starts a daemon that exposes a REST API (v1) with real-time streaming via
10Server-Sent Events (SSE). This app connects to that server over HTTP, lets you
11pick a workspace (a directory on the server), and chat with the crush agent —
12sending prompts, receiving streamed responses, and approving/denying tool
13permission requests — all from your phone.
15```
16┌─────────────┐ HTTP + SSE ┌──────────────┐
17│ Android │ ──────────────────→ │ crush │
18│ (this app) │ ←────────────────── │ server │
19└─────────────┘ /v1/* REST + SSE └──────────────┘
20```
22## Features
24- **Connect screen** — enter the server host and workspace path; connection
25 settings are persisted via DataStore.
26- **Chat UI** — message bubbles for user/assistant/tool messages, with
27 reasoning, tool calls, and tool results rendered inline.
28- **Session management** — create new sessions and switch between existing ones.
29- **SSE event streaming** — real-time message updates, agent events, and
30 run-completion via the `/v1/workspaces/{id}/events` endpoint.
31- **Permission requests** — tool permission requests from the agent surface as
32 dialogs; allow or deny with one tap.
33- **Cancel runs** — stop a busy agent run mid-stream.
35## Building
37This project uses Nix for its dev shell (see `flake.nix`). The SDK platform
38directory from Nix is named `android-37.0`, but AGP expects `android-37`, so a
39small local SDK overlay is needed:
41```sh
42# Enter the dev shell
43nix develop
45# Create the corrected SDK overlay (one-time)
46SDK_ROOT="$ANDROID_HOME"
47LOCAL_SDK=".android-sdk"
48mkdir -p "$LOCAL_SDK/platforms"
49for d in build-tools cmdline-tools platform-tools tools licenses cmake; do
50 ln -s "$SDK_ROOT/$d" "$LOCAL_SDK/$d"
51done
52cp -rL "$SDK_ROOT/platforms/android-37.0" "$LOCAL_SDK/platforms/android-37"
53sed -i 's/AndroidVersion.ApiLevel=37\.0/AndroidVersion.ApiLevel=37/' \
54 "$LOCAL_SDK/platforms/android-37/source.properties"
55sed -i 's/<api-level>37\.0<\/api-level>/<api-level>37<\/api-level>/' \
56 "$LOCAL_SDK/platforms/android-37/package.xml"
58# Point Gradle at the overlay
59echo "sdk.dir=$LOCAL_SDK" > local.properties
61# Build
62./gradlew assembleDebug
63```
65The debug APK lands at `app/build/outputs/apk/debug/app-debug.apk`.
67## Using
691. Start the crush server on a machine reachable from your phone:
70 ```sh
71 crush server --host 0.0.0.0:8080
72 ```
732. Install the APK on your Android device.
743. Open the app, enter `http://<server-ip>:8080` and the workspace path
75 (e.g. `/home/you/project`).
764. Tap **Connect**, then **+** to create a session, and start chatting.
78## Project structure
80```
81app/src/main/java/soy/iko/crush/
82├── CrushApp.kt # Application + ViewModel factory
83├── MainActivity.kt # Single-activity Compose host
84├── proto/ # Protocol model classes (match crush's Go structs)
85│ ├── Workspace.kt
86│ ├── Session.kt
87│ ├── Message.kt # Message, ContentPart, PartWrapper (discriminated union)
88│ ├── Events.kt # AgentEvent, RunComplete, PermissionRequest, etc.
89│ ├── Payload.kt # SSE payload envelope (PayloadType discriminator)
90│ └── Control.kt
91├── net/
92│ ├── CrushJson.kt # Shared kotlinx.serialization Json instance
93│ ├── CrushApiClient.kt # OkHttp REST client for the v1 API
94│ └── EventStream.kt # SSE parsing → Flow<Payload>
95├── data/
96│ ├── CrushRepository.kt # State aggregator: API + SSE → StateFlows
97│ └── SettingsStore.kt # DataStore preferences (host, workspace)
98└── ui/
99 ├── ChatViewModel.kt # ViewModel exposing ChatUiState
100 ├── MessageRenderer.kt # Flattens message parts → display text
101 ├── theme/CrushTheme.kt # Material 3 color scheme
102 └── screens/
103 ├── ConnectScreen.kt # Server connection form
104 └── ChatScreen.kt # Chat + session picker + permission dialogs
105```
107## Protocol notes
109The crush server uses a discriminated-union pattern for message parts. Each part
110is wrapped as `{"type": "text"|"tool_call"|"tool_result"|…, "data": {…}}`. The
111Kotlin `ContentPart` data class has all possible fields as nullable, and
112`PartWrapper` carries the type discriminator. SSE events are wrapped in a
113`Payload` envelope: `{"type": "message"|"session"|"permission_request"|…,
114"payload": {…}}`.